From b3ae2910260c364d4ce164dbc728328e05fa45ed Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 31 Jan 2024 14:35:21 +0100 Subject: [PATCH 001/750] Stop using the deprecated validate method --- .../org/odk/collect/android/formentry/FormEntryUseCases.kt | 2 +- .../org/odk/collect/android/formentry/FormEntryViewModel.java | 2 +- .../org/odk/collect/android/javarosawrapper/FormController.kt | 2 +- .../android/javarosawrapper/JavaRosaFormController.java | 4 ++-- .../java/org/odk/collect/android/tasks/SaveFormToDisk.java | 2 +- .../collect/android/javarosawrapper/FakeFormController.java | 2 +- .../collect/android/javarosawrapper/FormControllerTest.java | 4 ++-- .../org/odk/collect/android/utilities/StubFormController.kt | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryUseCases.kt b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryUseCases.kt index 35780b20124..da9a88b1933 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryUseCases.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryUseCases.kt @@ -180,7 +180,7 @@ object FormEntryUseCases { formController: FormController, entitiesRepository: EntitiesRepository ): Boolean { - val validationResult = formController.validateAnswers(markCompleted = true, moveToInvalidIndex = false) + val validationResult = formController.validateAnswers(false) if (validationResult is FailedValidationResult) { return false } diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryViewModel.java b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryViewModel.java index 1bd7fbb1576..2db6511e19d 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryViewModel.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryViewModel.java @@ -363,7 +363,7 @@ public void validate() { () -> { ValidationResult result = null; try { - result = formController.validateAnswers(true, true); + result = formController.validateAnswers(true); } catch (JavaRosaException e) { error.postValue(new FormError.NonFatal(e.getMessage())); } diff --git a/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormController.kt b/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormController.kt index 32d86753a3a..c14fe182990 100644 --- a/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormController.kt +++ b/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormController.kt @@ -139,7 +139,7 @@ interface FormController { * type. */ @Throws(JavaRosaException::class) - fun validateAnswers(markCompleted: Boolean, moveToInvalidIndex: Boolean): ValidationResult + fun validateAnswers(moveToInvalidIndex: Boolean): ValidationResult /** * saveAnswer attempts to save the current answer into the data model without doing any diff --git a/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/JavaRosaFormController.java b/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/JavaRosaFormController.java index 385a3a18161..2769df6496d 100644 --- a/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/JavaRosaFormController.java +++ b/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/JavaRosaFormController.java @@ -395,9 +395,9 @@ public int answerQuestion(FormIndex index, IAnswerData data) throws JavaRosaExce } } - public ValidationResult validateAnswers(boolean markCompleted, boolean moveToInvalidIndex) throws JavaRosaException { + public ValidationResult validateAnswers(boolean moveToInvalidIndex) throws JavaRosaException { try { - ValidateOutcome validateOutcome = getFormDef().validate(markCompleted); + ValidateOutcome validateOutcome = getFormDef().validate(); if (validateOutcome != null) { if (moveToInvalidIndex) { this.jumpToIndex(validateOutcome.failedPrompt); diff --git a/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java b/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java index 4bc3956a65d..2d3dbc20be7 100644 --- a/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java +++ b/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java @@ -115,7 +115,7 @@ public SaveToDiskResult saveForm(FormSaver.ProgressListener progressListener) { ValidationResult validationResult; try { - validationResult = formController.validateAnswers(true, shouldFinalize); + validationResult = formController.validateAnswers(shouldFinalize); if (shouldFinalize && validationResult instanceof FailedValidationResult) { // validation failed, pass specific failure saveToDiskResult.setSaveResult(((FailedValidationResult) validationResult).getStatus(), shouldFinalize); diff --git a/collect_app/src/test/java/org/odk/collect/android/javarosawrapper/FakeFormController.java b/collect_app/src/test/java/org/odk/collect/android/javarosawrapper/FakeFormController.java index 614c141897f..1015352c0ca 100644 --- a/collect_app/src/test/java/org/odk/collect/android/javarosawrapper/FakeFormController.java +++ b/collect_app/src/test/java/org/odk/collect/android/javarosawrapper/FakeFormController.java @@ -113,7 +113,7 @@ public FormEntryPrompt getQuestionPrompt() { @NonNull @Override - public ValidationResult validateAnswers(boolean markCompleted, boolean moveToInvalidIndex) throws JavaRosaException { + public ValidationResult validateAnswers(boolean moveToInvalidIndex) throws JavaRosaException { if (validationError != null) { throw validationError; } else { diff --git a/collect_app/src/test/java/org/odk/collect/android/javarosawrapper/FormControllerTest.java b/collect_app/src/test/java/org/odk/collect/android/javarosawrapper/FormControllerTest.java index f7c950ff49e..7e6236b51c5 100644 --- a/collect_app/src/test/java/org/odk/collect/android/javarosawrapper/FormControllerTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/javarosawrapper/FormControllerTest.java @@ -33,7 +33,7 @@ public void validateAnswers_shouldNotChangeFormIndexToTheIndexOfInvalidQuestionI formController.stepToNextScreenEvent(); assertThat(formController.getFormIndex().toString(), equalTo("0, ")); - formController.validateAnswers(true, false); + formController.validateAnswers(false); assertThat(formController.getFormIndex().toString(), equalTo("0, ")); } @@ -48,7 +48,7 @@ public void validateAnswers_shouldChangeFormIndexToTheIndexOfInvalidQuestionIfAs formController.stepToNextScreenEvent(); assertThat(formController.getFormIndex().toString(), equalTo("0, ")); - formController.validateAnswers(true, true); + formController.validateAnswers(true); assertThat(formController.getFormIndex().toString(), equalTo("1, ")); } diff --git a/collect_app/src/test/java/org/odk/collect/android/utilities/StubFormController.kt b/collect_app/src/test/java/org/odk/collect/android/utilities/StubFormController.kt index b68da29fcf8..36db06ef367 100644 --- a/collect_app/src/test/java/org/odk/collect/android/utilities/StubFormController.kt +++ b/collect_app/src/test/java/org/odk/collect/android/utilities/StubFormController.kt @@ -77,7 +77,7 @@ open class StubFormController : FormController { override fun answerQuestion(index: FormIndex?, data: IAnswerData?): Int = -1 @Throws(JavaRosaException::class) - override fun validateAnswers(markCompleted: Boolean, moveToInvalidIndex: Boolean): ValidationResult = SuccessValidationResult + override fun validateAnswers(moveToInvalidIndex: Boolean): ValidationResult = SuccessValidationResult override fun saveAnswer(index: FormIndex?, data: IAnswerData?): Boolean = false From f5233f5e6fc140c9b1f51067794bbd86cba054f8 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 27 Feb 2024 16:25:43 +0100 Subject: [PATCH 002/750] Fixed the audiorecorder module name --- {audiorecorder => audio-recorder}/.gitignore | 0 {audiorecorder => audio-recorder}/build.gradle.kts | 0 {audiorecorder => audio-recorder}/consumer-rules.pro | 0 {audiorecorder => audio-recorder}/src/main/AndroidManifest.xml | 0 .../src/main/java/org/odk/collect/audiorecorder/DaggerSetup.kt | 0 .../mediarecorder/MediaRecorderRecordingResource.kt | 0 .../java/org/odk/collect/audiorecorder/recorder/Recorder.kt | 0 .../org/odk/collect/audiorecorder/recorder/RecordingResource.kt | 0 .../collect/audiorecorder/recorder/RecordingResourceRecorder.kt | 0 .../org/odk/collect/audiorecorder/recording/AudioRecorder.kt | 0 .../odk/collect/audiorecorder/recording/AudioRecorderFactory.kt | 0 .../odk/collect/audiorecorder/recording/AudioRecorderService.kt | 0 .../recording/internal/ForegroundServiceAudioRecorder.kt | 0 .../internal/RecordingForegroundServiceNotification.kt | 0 .../audiorecorder/recording/internal/RecordingRepository.kt | 0 .../odk/collect/audiorecorder/testsupport/StubAudioRecorder.kt | 0 .../audiorecorder/mediarecorder/AMRRecordingResourceTest.kt | 0 .../audiorecorder/recorder/RecordingResourceRecorderTest.kt | 0 .../odk/collect/audiorecorder/recording/AudioRecorderTest.kt | 0 .../recording/internal/AudioRecorderServiceTest.kt | 0 .../recording/internal/ForegroundServiceAudioRecorderTest.kt | 0 .../internal/RecordingForegroundServiceNotificationTest.kt | 0 .../java/org/odk/collect/audiorecorder/support/FakeRecorder.kt | 0 .../collect/audiorecorder/testsupport/RobolectricApplication.kt | 0 .../collect/audiorecorder/testsupport/StubAudioRecorderTest.kt | 0 .../src/test/resources/robolectric.properties | 0 collect_app/build.gradle | 2 +- settings.gradle | 2 +- 28 files changed, 2 insertions(+), 2 deletions(-) rename {audiorecorder => audio-recorder}/.gitignore (100%) rename {audiorecorder => audio-recorder}/build.gradle.kts (100%) rename {audiorecorder => audio-recorder}/consumer-rules.pro (100%) rename {audiorecorder => audio-recorder}/src/main/AndroidManifest.xml (100%) rename {audiorecorder => audio-recorder}/src/main/java/org/odk/collect/audiorecorder/DaggerSetup.kt (100%) rename {audiorecorder => audio-recorder}/src/main/java/org/odk/collect/audiorecorder/mediarecorder/MediaRecorderRecordingResource.kt (100%) rename {audiorecorder => audio-recorder}/src/main/java/org/odk/collect/audiorecorder/recorder/Recorder.kt (100%) rename {audiorecorder => audio-recorder}/src/main/java/org/odk/collect/audiorecorder/recorder/RecordingResource.kt (100%) rename {audiorecorder => audio-recorder}/src/main/java/org/odk/collect/audiorecorder/recorder/RecordingResourceRecorder.kt (100%) rename {audiorecorder => audio-recorder}/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorder.kt (100%) rename {audiorecorder => audio-recorder}/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorderFactory.kt (100%) rename {audiorecorder => audio-recorder}/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorderService.kt (100%) rename {audiorecorder => audio-recorder}/src/main/java/org/odk/collect/audiorecorder/recording/internal/ForegroundServiceAudioRecorder.kt (100%) rename {audiorecorder => audio-recorder}/src/main/java/org/odk/collect/audiorecorder/recording/internal/RecordingForegroundServiceNotification.kt (100%) rename {audiorecorder => audio-recorder}/src/main/java/org/odk/collect/audiorecorder/recording/internal/RecordingRepository.kt (100%) rename {audiorecorder => audio-recorder}/src/main/java/org/odk/collect/audiorecorder/testsupport/StubAudioRecorder.kt (100%) rename {audiorecorder => audio-recorder}/src/test/java/org/odk/collect/audiorecorder/mediarecorder/AMRRecordingResourceTest.kt (100%) rename {audiorecorder => audio-recorder}/src/test/java/org/odk/collect/audiorecorder/recorder/RecordingResourceRecorderTest.kt (100%) rename {audiorecorder => audio-recorder}/src/test/java/org/odk/collect/audiorecorder/recording/AudioRecorderTest.kt (100%) rename {audiorecorder => audio-recorder}/src/test/java/org/odk/collect/audiorecorder/recording/internal/AudioRecorderServiceTest.kt (100%) rename {audiorecorder => audio-recorder}/src/test/java/org/odk/collect/audiorecorder/recording/internal/ForegroundServiceAudioRecorderTest.kt (100%) rename {audiorecorder => audio-recorder}/src/test/java/org/odk/collect/audiorecorder/recording/internal/RecordingForegroundServiceNotificationTest.kt (100%) rename {audiorecorder => audio-recorder}/src/test/java/org/odk/collect/audiorecorder/support/FakeRecorder.kt (100%) rename {audiorecorder => audio-recorder}/src/test/java/org/odk/collect/audiorecorder/testsupport/RobolectricApplication.kt (100%) rename {audiorecorder => audio-recorder}/src/test/java/org/odk/collect/audiorecorder/testsupport/StubAudioRecorderTest.kt (100%) rename {audiorecorder => audio-recorder}/src/test/resources/robolectric.properties (100%) diff --git a/audiorecorder/.gitignore b/audio-recorder/.gitignore similarity index 100% rename from audiorecorder/.gitignore rename to audio-recorder/.gitignore diff --git a/audiorecorder/build.gradle.kts b/audio-recorder/build.gradle.kts similarity index 100% rename from audiorecorder/build.gradle.kts rename to audio-recorder/build.gradle.kts diff --git a/audiorecorder/consumer-rules.pro b/audio-recorder/consumer-rules.pro similarity index 100% rename from audiorecorder/consumer-rules.pro rename to audio-recorder/consumer-rules.pro diff --git a/audiorecorder/src/main/AndroidManifest.xml b/audio-recorder/src/main/AndroidManifest.xml similarity index 100% rename from audiorecorder/src/main/AndroidManifest.xml rename to audio-recorder/src/main/AndroidManifest.xml diff --git a/audiorecorder/src/main/java/org/odk/collect/audiorecorder/DaggerSetup.kt b/audio-recorder/src/main/java/org/odk/collect/audiorecorder/DaggerSetup.kt similarity index 100% rename from audiorecorder/src/main/java/org/odk/collect/audiorecorder/DaggerSetup.kt rename to audio-recorder/src/main/java/org/odk/collect/audiorecorder/DaggerSetup.kt diff --git a/audiorecorder/src/main/java/org/odk/collect/audiorecorder/mediarecorder/MediaRecorderRecordingResource.kt b/audio-recorder/src/main/java/org/odk/collect/audiorecorder/mediarecorder/MediaRecorderRecordingResource.kt similarity index 100% rename from audiorecorder/src/main/java/org/odk/collect/audiorecorder/mediarecorder/MediaRecorderRecordingResource.kt rename to audio-recorder/src/main/java/org/odk/collect/audiorecorder/mediarecorder/MediaRecorderRecordingResource.kt diff --git a/audiorecorder/src/main/java/org/odk/collect/audiorecorder/recorder/Recorder.kt b/audio-recorder/src/main/java/org/odk/collect/audiorecorder/recorder/Recorder.kt similarity index 100% rename from audiorecorder/src/main/java/org/odk/collect/audiorecorder/recorder/Recorder.kt rename to audio-recorder/src/main/java/org/odk/collect/audiorecorder/recorder/Recorder.kt diff --git a/audiorecorder/src/main/java/org/odk/collect/audiorecorder/recorder/RecordingResource.kt b/audio-recorder/src/main/java/org/odk/collect/audiorecorder/recorder/RecordingResource.kt similarity index 100% rename from audiorecorder/src/main/java/org/odk/collect/audiorecorder/recorder/RecordingResource.kt rename to audio-recorder/src/main/java/org/odk/collect/audiorecorder/recorder/RecordingResource.kt diff --git a/audiorecorder/src/main/java/org/odk/collect/audiorecorder/recorder/RecordingResourceRecorder.kt b/audio-recorder/src/main/java/org/odk/collect/audiorecorder/recorder/RecordingResourceRecorder.kt similarity index 100% rename from audiorecorder/src/main/java/org/odk/collect/audiorecorder/recorder/RecordingResourceRecorder.kt rename to audio-recorder/src/main/java/org/odk/collect/audiorecorder/recorder/RecordingResourceRecorder.kt diff --git a/audiorecorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorder.kt b/audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorder.kt similarity index 100% rename from audiorecorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorder.kt rename to audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorder.kt diff --git a/audiorecorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorderFactory.kt b/audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorderFactory.kt similarity index 100% rename from audiorecorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorderFactory.kt rename to audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorderFactory.kt diff --git a/audiorecorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorderService.kt b/audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorderService.kt similarity index 100% rename from audiorecorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorderService.kt rename to audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorderService.kt diff --git a/audiorecorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/ForegroundServiceAudioRecorder.kt b/audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/ForegroundServiceAudioRecorder.kt similarity index 100% rename from audiorecorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/ForegroundServiceAudioRecorder.kt rename to audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/ForegroundServiceAudioRecorder.kt diff --git a/audiorecorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/RecordingForegroundServiceNotification.kt b/audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/RecordingForegroundServiceNotification.kt similarity index 100% rename from audiorecorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/RecordingForegroundServiceNotification.kt rename to audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/RecordingForegroundServiceNotification.kt diff --git a/audiorecorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/RecordingRepository.kt b/audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/RecordingRepository.kt similarity index 100% rename from audiorecorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/RecordingRepository.kt rename to audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/RecordingRepository.kt diff --git a/audiorecorder/src/main/java/org/odk/collect/audiorecorder/testsupport/StubAudioRecorder.kt b/audio-recorder/src/main/java/org/odk/collect/audiorecorder/testsupport/StubAudioRecorder.kt similarity index 100% rename from audiorecorder/src/main/java/org/odk/collect/audiorecorder/testsupport/StubAudioRecorder.kt rename to audio-recorder/src/main/java/org/odk/collect/audiorecorder/testsupport/StubAudioRecorder.kt diff --git a/audiorecorder/src/test/java/org/odk/collect/audiorecorder/mediarecorder/AMRRecordingResourceTest.kt b/audio-recorder/src/test/java/org/odk/collect/audiorecorder/mediarecorder/AMRRecordingResourceTest.kt similarity index 100% rename from audiorecorder/src/test/java/org/odk/collect/audiorecorder/mediarecorder/AMRRecordingResourceTest.kt rename to audio-recorder/src/test/java/org/odk/collect/audiorecorder/mediarecorder/AMRRecordingResourceTest.kt diff --git a/audiorecorder/src/test/java/org/odk/collect/audiorecorder/recorder/RecordingResourceRecorderTest.kt b/audio-recorder/src/test/java/org/odk/collect/audiorecorder/recorder/RecordingResourceRecorderTest.kt similarity index 100% rename from audiorecorder/src/test/java/org/odk/collect/audiorecorder/recorder/RecordingResourceRecorderTest.kt rename to audio-recorder/src/test/java/org/odk/collect/audiorecorder/recorder/RecordingResourceRecorderTest.kt diff --git a/audiorecorder/src/test/java/org/odk/collect/audiorecorder/recording/AudioRecorderTest.kt b/audio-recorder/src/test/java/org/odk/collect/audiorecorder/recording/AudioRecorderTest.kt similarity index 100% rename from audiorecorder/src/test/java/org/odk/collect/audiorecorder/recording/AudioRecorderTest.kt rename to audio-recorder/src/test/java/org/odk/collect/audiorecorder/recording/AudioRecorderTest.kt diff --git a/audiorecorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/AudioRecorderServiceTest.kt b/audio-recorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/AudioRecorderServiceTest.kt similarity index 100% rename from audiorecorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/AudioRecorderServiceTest.kt rename to audio-recorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/AudioRecorderServiceTest.kt diff --git a/audiorecorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/ForegroundServiceAudioRecorderTest.kt b/audio-recorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/ForegroundServiceAudioRecorderTest.kt similarity index 100% rename from audiorecorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/ForegroundServiceAudioRecorderTest.kt rename to audio-recorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/ForegroundServiceAudioRecorderTest.kt diff --git a/audiorecorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/RecordingForegroundServiceNotificationTest.kt b/audio-recorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/RecordingForegroundServiceNotificationTest.kt similarity index 100% rename from audiorecorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/RecordingForegroundServiceNotificationTest.kt rename to audio-recorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/RecordingForegroundServiceNotificationTest.kt diff --git a/audiorecorder/src/test/java/org/odk/collect/audiorecorder/support/FakeRecorder.kt b/audio-recorder/src/test/java/org/odk/collect/audiorecorder/support/FakeRecorder.kt similarity index 100% rename from audiorecorder/src/test/java/org/odk/collect/audiorecorder/support/FakeRecorder.kt rename to audio-recorder/src/test/java/org/odk/collect/audiorecorder/support/FakeRecorder.kt diff --git a/audiorecorder/src/test/java/org/odk/collect/audiorecorder/testsupport/RobolectricApplication.kt b/audio-recorder/src/test/java/org/odk/collect/audiorecorder/testsupport/RobolectricApplication.kt similarity index 100% rename from audiorecorder/src/test/java/org/odk/collect/audiorecorder/testsupport/RobolectricApplication.kt rename to audio-recorder/src/test/java/org/odk/collect/audiorecorder/testsupport/RobolectricApplication.kt diff --git a/audiorecorder/src/test/java/org/odk/collect/audiorecorder/testsupport/StubAudioRecorderTest.kt b/audio-recorder/src/test/java/org/odk/collect/audiorecorder/testsupport/StubAudioRecorderTest.kt similarity index 100% rename from audiorecorder/src/test/java/org/odk/collect/audiorecorder/testsupport/StubAudioRecorderTest.kt rename to audio-recorder/src/test/java/org/odk/collect/audiorecorder/testsupport/StubAudioRecorderTest.kt diff --git a/audiorecorder/src/test/resources/robolectric.properties b/audio-recorder/src/test/resources/robolectric.properties similarity index 100% rename from audiorecorder/src/test/resources/robolectric.properties rename to audio-recorder/src/test/resources/robolectric.properties diff --git a/collect_app/build.gradle b/collect_app/build.gradle index cb08809c4c8..505ba2042b1 100644 --- a/collect_app/build.gradle +++ b/collect_app/build.gradle @@ -240,7 +240,7 @@ dependencies { implementation project(':analytics') implementation project(':audioclips') implementation project(':forms') - implementation project(':audiorecorder') + implementation project(':audio-recorder') implementation project(':projects') implementation project(':location') implementation project(':geo') diff --git a/settings.gradle b/settings.gradle index 3d340523d42..856e753934e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,7 +4,7 @@ include ':shared' include ':forms' include ':analytics' include ':androidshared' -include ':audiorecorder' +include ':audio-recorder' include ':test-shared' include ':audioclips' include ':strings' From e7adec8b67603bd0aed0f6f3b324d09893b3247c Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 28 Feb 2024 17:44:52 +0100 Subject: [PATCH 003/750] Fixed the audioclips module name --- {audioclips => audio-clips}/.gitignore | 0 {audioclips => audio-clips}/build.gradle.kts | 0 {audioclips => audio-clips}/consumer-rules.pro | 0 {audioclips => audio-clips}/src/main/AndroidManifest.xml | 0 .../main/java/org/odk/collect/audioclips/AudioClipViewModel.kt | 0 .../src/main/java/org/odk/collect/audioclips/Clip.kt | 0 .../java/org/odk/collect/audioclips/PlaybackFailedException.kt | 0 .../java/org/odk/collect/audioclips/AudioClipViewModelTest.kt | 0 collect_app/build.gradle | 2 +- settings.gradle | 2 +- 10 files changed, 2 insertions(+), 2 deletions(-) rename {audioclips => audio-clips}/.gitignore (100%) rename {audioclips => audio-clips}/build.gradle.kts (100%) rename {audioclips => audio-clips}/consumer-rules.pro (100%) rename {audioclips => audio-clips}/src/main/AndroidManifest.xml (100%) rename {audioclips => audio-clips}/src/main/java/org/odk/collect/audioclips/AudioClipViewModel.kt (100%) rename {audioclips => audio-clips}/src/main/java/org/odk/collect/audioclips/Clip.kt (100%) rename {audioclips => audio-clips}/src/main/java/org/odk/collect/audioclips/PlaybackFailedException.kt (100%) rename {audioclips => audio-clips}/src/test/java/org/odk/collect/audioclips/AudioClipViewModelTest.kt (100%) diff --git a/audioclips/.gitignore b/audio-clips/.gitignore similarity index 100% rename from audioclips/.gitignore rename to audio-clips/.gitignore diff --git a/audioclips/build.gradle.kts b/audio-clips/build.gradle.kts similarity index 100% rename from audioclips/build.gradle.kts rename to audio-clips/build.gradle.kts diff --git a/audioclips/consumer-rules.pro b/audio-clips/consumer-rules.pro similarity index 100% rename from audioclips/consumer-rules.pro rename to audio-clips/consumer-rules.pro diff --git a/audioclips/src/main/AndroidManifest.xml b/audio-clips/src/main/AndroidManifest.xml similarity index 100% rename from audioclips/src/main/AndroidManifest.xml rename to audio-clips/src/main/AndroidManifest.xml diff --git a/audioclips/src/main/java/org/odk/collect/audioclips/AudioClipViewModel.kt b/audio-clips/src/main/java/org/odk/collect/audioclips/AudioClipViewModel.kt similarity index 100% rename from audioclips/src/main/java/org/odk/collect/audioclips/AudioClipViewModel.kt rename to audio-clips/src/main/java/org/odk/collect/audioclips/AudioClipViewModel.kt diff --git a/audioclips/src/main/java/org/odk/collect/audioclips/Clip.kt b/audio-clips/src/main/java/org/odk/collect/audioclips/Clip.kt similarity index 100% rename from audioclips/src/main/java/org/odk/collect/audioclips/Clip.kt rename to audio-clips/src/main/java/org/odk/collect/audioclips/Clip.kt diff --git a/audioclips/src/main/java/org/odk/collect/audioclips/PlaybackFailedException.kt b/audio-clips/src/main/java/org/odk/collect/audioclips/PlaybackFailedException.kt similarity index 100% rename from audioclips/src/main/java/org/odk/collect/audioclips/PlaybackFailedException.kt rename to audio-clips/src/main/java/org/odk/collect/audioclips/PlaybackFailedException.kt diff --git a/audioclips/src/test/java/org/odk/collect/audioclips/AudioClipViewModelTest.kt b/audio-clips/src/test/java/org/odk/collect/audioclips/AudioClipViewModelTest.kt similarity index 100% rename from audioclips/src/test/java/org/odk/collect/audioclips/AudioClipViewModelTest.kt rename to audio-clips/src/test/java/org/odk/collect/audioclips/AudioClipViewModelTest.kt diff --git a/collect_app/build.gradle b/collect_app/build.gradle index 505ba2042b1..19681d2f58a 100644 --- a/collect_app/build.gradle +++ b/collect_app/build.gradle @@ -238,7 +238,7 @@ dependencies { implementation project(':material') implementation project(':async') implementation project(':analytics') - implementation project(':audioclips') + implementation project(':audio-clips') implementation project(':forms') implementation project(':audio-recorder') implementation project(':projects') diff --git a/settings.gradle b/settings.gradle index 856e753934e..3baf1e9cb1c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ include ':analytics' include ':androidshared' include ':audio-recorder' include ':test-shared' -include ':audioclips' +include ':audio-clips' include ':strings' include ':async' include ':collect_app' From 7057ebce66124a3d56ef4eab1e31150083e728a8 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 28 Feb 2024 17:47:02 +0100 Subject: [PATCH 004/750] Fixed the formstest module name --- collect_app/build.gradle | 2 +- {formstest => forms-test}/.gitignore | 0 {formstest => forms-test}/README.md | 0 {formstest => forms-test}/build.gradle.kts | 0 .../src/main/java/org/odk/collect/formstest/FormFixtures.kt | 0 .../src/main/java/org/odk/collect/formstest/FormUtils.kt | 0 .../java/org/odk/collect/formstest/FormsRepositoryTest.java | 0 .../java/org/odk/collect/formstest/InMemFormsRepository.java | 0 .../org/odk/collect/formstest/InMemInstancesRepository.java | 0 .../src/main/java/org/odk/collect/formstest/InstanceFixtures.kt | 0 .../src/main/java/org/odk/collect/formstest/InstanceUtils.kt | 0 .../java/org/odk/collect/formstest/InstancesRepositoryTest.java | 0 .../org/odk/collect/formstest/InMemFormsRepositoryTest.java | 0 .../org/odk/collect/formstest/InMemInstancesRepositoryTest.java | 0 settings.gradle | 2 +- 15 files changed, 2 insertions(+), 2 deletions(-) rename {formstest => forms-test}/.gitignore (100%) rename {formstest => forms-test}/README.md (100%) rename {formstest => forms-test}/build.gradle.kts (100%) rename {formstest => forms-test}/src/main/java/org/odk/collect/formstest/FormFixtures.kt (100%) rename {formstest => forms-test}/src/main/java/org/odk/collect/formstest/FormUtils.kt (100%) rename {formstest => forms-test}/src/main/java/org/odk/collect/formstest/FormsRepositoryTest.java (100%) rename {formstest => forms-test}/src/main/java/org/odk/collect/formstest/InMemFormsRepository.java (100%) rename {formstest => forms-test}/src/main/java/org/odk/collect/formstest/InMemInstancesRepository.java (100%) rename {formstest => forms-test}/src/main/java/org/odk/collect/formstest/InstanceFixtures.kt (100%) rename {formstest => forms-test}/src/main/java/org/odk/collect/formstest/InstanceUtils.kt (100%) rename {formstest => forms-test}/src/main/java/org/odk/collect/formstest/InstancesRepositoryTest.java (100%) rename {formstest => forms-test}/src/test/java/org/odk/collect/formstest/InMemFormsRepositoryTest.java (100%) rename {formstest => forms-test}/src/test/java/org/odk/collect/formstest/InMemInstancesRepositoryTest.java (100%) diff --git a/collect_app/build.gradle b/collect_app/build.gradle index 19681d2f58a..6b090adc106 100644 --- a/collect_app/build.gradle +++ b/collect_app/build.gradle @@ -341,7 +341,7 @@ dependencies { implementation Dependencies.splashscreen - testImplementation project(':formstest') + testImplementation project(':forms-test') // Testing-only dependencies testImplementation Dependencies.junit diff --git a/formstest/.gitignore b/forms-test/.gitignore similarity index 100% rename from formstest/.gitignore rename to forms-test/.gitignore diff --git a/formstest/README.md b/forms-test/README.md similarity index 100% rename from formstest/README.md rename to forms-test/README.md diff --git a/formstest/build.gradle.kts b/forms-test/build.gradle.kts similarity index 100% rename from formstest/build.gradle.kts rename to forms-test/build.gradle.kts diff --git a/formstest/src/main/java/org/odk/collect/formstest/FormFixtures.kt b/forms-test/src/main/java/org/odk/collect/formstest/FormFixtures.kt similarity index 100% rename from formstest/src/main/java/org/odk/collect/formstest/FormFixtures.kt rename to forms-test/src/main/java/org/odk/collect/formstest/FormFixtures.kt diff --git a/formstest/src/main/java/org/odk/collect/formstest/FormUtils.kt b/forms-test/src/main/java/org/odk/collect/formstest/FormUtils.kt similarity index 100% rename from formstest/src/main/java/org/odk/collect/formstest/FormUtils.kt rename to forms-test/src/main/java/org/odk/collect/formstest/FormUtils.kt diff --git a/formstest/src/main/java/org/odk/collect/formstest/FormsRepositoryTest.java b/forms-test/src/main/java/org/odk/collect/formstest/FormsRepositoryTest.java similarity index 100% rename from formstest/src/main/java/org/odk/collect/formstest/FormsRepositoryTest.java rename to forms-test/src/main/java/org/odk/collect/formstest/FormsRepositoryTest.java diff --git a/formstest/src/main/java/org/odk/collect/formstest/InMemFormsRepository.java b/forms-test/src/main/java/org/odk/collect/formstest/InMemFormsRepository.java similarity index 100% rename from formstest/src/main/java/org/odk/collect/formstest/InMemFormsRepository.java rename to forms-test/src/main/java/org/odk/collect/formstest/InMemFormsRepository.java diff --git a/formstest/src/main/java/org/odk/collect/formstest/InMemInstancesRepository.java b/forms-test/src/main/java/org/odk/collect/formstest/InMemInstancesRepository.java similarity index 100% rename from formstest/src/main/java/org/odk/collect/formstest/InMemInstancesRepository.java rename to forms-test/src/main/java/org/odk/collect/formstest/InMemInstancesRepository.java diff --git a/formstest/src/main/java/org/odk/collect/formstest/InstanceFixtures.kt b/forms-test/src/main/java/org/odk/collect/formstest/InstanceFixtures.kt similarity index 100% rename from formstest/src/main/java/org/odk/collect/formstest/InstanceFixtures.kt rename to forms-test/src/main/java/org/odk/collect/formstest/InstanceFixtures.kt diff --git a/formstest/src/main/java/org/odk/collect/formstest/InstanceUtils.kt b/forms-test/src/main/java/org/odk/collect/formstest/InstanceUtils.kt similarity index 100% rename from formstest/src/main/java/org/odk/collect/formstest/InstanceUtils.kt rename to forms-test/src/main/java/org/odk/collect/formstest/InstanceUtils.kt diff --git a/formstest/src/main/java/org/odk/collect/formstest/InstancesRepositoryTest.java b/forms-test/src/main/java/org/odk/collect/formstest/InstancesRepositoryTest.java similarity index 100% rename from formstest/src/main/java/org/odk/collect/formstest/InstancesRepositoryTest.java rename to forms-test/src/main/java/org/odk/collect/formstest/InstancesRepositoryTest.java diff --git a/formstest/src/test/java/org/odk/collect/formstest/InMemFormsRepositoryTest.java b/forms-test/src/test/java/org/odk/collect/formstest/InMemFormsRepositoryTest.java similarity index 100% rename from formstest/src/test/java/org/odk/collect/formstest/InMemFormsRepositoryTest.java rename to forms-test/src/test/java/org/odk/collect/formstest/InMemFormsRepositoryTest.java diff --git a/formstest/src/test/java/org/odk/collect/formstest/InMemInstancesRepositoryTest.java b/forms-test/src/test/java/org/odk/collect/formstest/InMemInstancesRepositoryTest.java similarity index 100% rename from formstest/src/test/java/org/odk/collect/formstest/InMemInstancesRepositoryTest.java rename to forms-test/src/test/java/org/odk/collect/formstest/InMemInstancesRepositoryTest.java diff --git a/settings.gradle b/settings.gradle index 3baf1e9cb1c..28910d469b2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ include ':projects' -include ':formstest' +include ':forms-test' include ':shared' include ':forms' include ':analytics' From 60194b30f21a2b60aedc71f0ba62eecdaf1f97c9 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Sat, 23 Dec 2023 19:19:37 +0100 Subject: [PATCH 005/750] Reworked image widgets --- .../formentry/FieldListUpdateTest.java | 4 +- .../android/regression/DrawWidgetTest.java | 8 +- .../regression/SignatureWidgetTest.java | 7 +- .../formentry/questions/WidgetViewUtils.kt | 11 -- .../android/widgets/AnnotateWidget.java | 128 +++++++----------- .../android/widgets/BaseImageWidget.java | 22 +-- .../collect/android/widgets/DrawWidget.java | 49 ++++--- .../collect/android/widgets/ImageWidget.java | 76 ++++------- .../android/widgets/SignatureWidget.java | 49 ++++--- .../src/main/res/layout/annotate_widget.xml | 64 +++++++++ .../src/main/res/layout/draw_widget.xml | 40 ++++++ .../src/main/res/layout/image_widget.xml | 52 +++++++ .../src/main/res/layout/signature_widget.xml | 40 ++++++ .../android/widgets/AnnotateWidgetTest.java | 34 ++--- .../android/widgets/DrawWidgetTest.java | 10 +- .../android/widgets/ImageWidgetTest.java | 18 +-- .../android/widgets/SignatureWidgetTest.java | 10 +- 17 files changed, 367 insertions(+), 255 deletions(-) create mode 100644 collect_app/src/main/res/layout/annotate_widget.xml create mode 100644 collect_app/src/main/res/layout/draw_widget.xml create mode 100644 collect_app/src/main/res/layout/image_widget.xml create mode 100644 collect_app/src/main/res/layout/signature_widget.xml diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java index da9c1c7d2f6..b5846cb45d5 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java @@ -316,10 +316,10 @@ public void questionsAppearingBeforeCurrentBinaryQuestion_ShouldNotChangeFocus() intending(not(isInternal())).respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null)); - onView(withId(R.id.capture_image)).perform(click()); + onView(withId(R.id.capture_button)).perform(click()); onView(withText("Target10-15")).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))); - onView(withId(R.id.capture_image)).check(matches(isCompletelyDisplayed())); + onView(withId(R.id.capture_button)).check(matches(isCompletelyDisplayed())); } @Test diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/regression/DrawWidgetTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/regression/DrawWidgetTest.java index b5e1995dc33..4374c1f6c63 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/regression/DrawWidgetTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/regression/DrawWidgetTest.java @@ -31,12 +31,12 @@ public void saveIgnoreDialog_ShouldUseBothOptions() { .clickGoToArrow() .clickOnText("Image widgets") .clickOnText("Draw widget") - .clickOnId(R.id.simple_button) + .clickOnId(R.id.draw_button) .waitForRotationToEnd() .pressBack(new SaveOrIgnoreDrawingDialog<>("Sketch Image", new FormEntryPage("All widgets"))) .clickDiscardChanges() .waitForRotationToEnd() - .clickOnId(R.id.simple_button) + .clickOnId(R.id.draw_button) .waitForRotationToEnd() .pressBack(new SaveOrIgnoreDrawingDialog<>("Sketch Image", new FormEntryPage("All widgets"))) .clickSaveChanges() @@ -55,7 +55,7 @@ public void setColor_ShouldSeeColorPicker() { .clickGoToArrow() .clickOnText("Image widgets") .clickOnText("Draw widget") - .clickOnId(R.id.simple_button) + .clickOnId(R.id.draw_button) .waitForRotationToEnd() .clickOnId(org.odk.collect.draw.R.id.fab_actions) .clickOnId(org.odk.collect.draw.R.id.fab_set_color) @@ -77,7 +77,7 @@ public void multiClickOnPlus_ShouldDisplayIcons() { .clickGoToArrow() .clickOnText("Image widgets") .clickOnText("Draw widget") - .clickOnId(R.id.simple_button) + .clickOnId(R.id.draw_button) .waitForRotationToEnd() .clickOnId(org.odk.collect.draw.R.id.fab_actions) .assertText(org.odk.collect.strings.R.string.set_color) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/regression/SignatureWidgetTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/regression/SignatureWidgetTest.java index 09af9a4487b..334b023698e 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/regression/SignatureWidgetTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/regression/SignatureWidgetTest.java @@ -6,6 +6,7 @@ import org.junit.Test; import org.junit.rules.RuleChain; import org.junit.runner.RunWith; +import org.odk.collect.android.R; import org.odk.collect.android.support.pages.FormEntryPage; import org.odk.collect.android.support.pages.SaveOrIgnoreDrawingDialog; import org.odk.collect.android.support.rules.CollectTestRule; @@ -31,14 +32,14 @@ public void saveIgnoreDialog_ShouldUseBothOptions() { .clickGoToArrow() .clickOnText("Image widgets") .clickOnQuestion("Signature widget") - .clickWidgetButton() + .clickOnId(R.id.sign_button) .waitForRotationToEnd() .pressBack(new SaveOrIgnoreDrawingDialog<>("Gather Signature", new FormEntryPage("All widgets"))) .checkIsTranslationDisplayed("Exit Gather Signature", "Salir Adjuntar firma") .assertText(org.odk.collect.strings.R.string.keep_changes) .clickDiscardChanges() .waitForRotationToEnd() - .clickWidgetButton() + .clickOnId(R.id.sign_button) .waitForRotationToEnd() .pressBack(new SaveOrIgnoreDrawingDialog<>("Gather Signature", new FormEntryPage("All widgets"))) .clickSaveChanges() @@ -58,7 +59,7 @@ public void multiClickOnPlus_ShouldDisplayIcons() { .clickGoToArrow() .clickOnText("Image widgets") .clickOnQuestion("Signature widget") - .clickWidgetButton() + .clickOnId(R.id.sign_button) .waitForRotationToEnd() .clickOnId(org.odk.collect.draw.R.id.fab_actions) .checkIsIdDisplayed(org.odk.collect.draw.R.id.fab_save_and_close) diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/questions/WidgetViewUtils.kt b/collect_app/src/main/java/org/odk/collect/android/formentry/questions/WidgetViewUtils.kt index 629cfc5caed..d8f56d9b2d7 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/questions/WidgetViewUtils.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/questions/WidgetViewUtils.kt @@ -5,7 +5,6 @@ import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.widget.Button -import android.widget.ImageView import android.widget.TableLayout import android.widget.TextView import androidx.annotation.IdRes @@ -28,16 +27,6 @@ object WidgetViewUtils { } } - @JvmStatic - fun createAnswerImageView(context: Context): ImageView { - return ImageView(context).apply { - id = View.generateViewId() - tag = "ImageView" - setPadding(10, 10, 10, 10) - adjustViewBounds = true - } - } - @JvmStatic @JvmOverloads fun createSimpleButton(context: Context, readOnly: Boolean, text: String?, listener: ButtonClickListener, addMargin: Boolean, @IdRes withId: Int = R.id.simple_button): Button { diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/AnnotateWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/AnnotateWidget.java index 65026144bfb..c2752e05db8 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/AnnotateWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/AnnotateWidget.java @@ -16,7 +16,6 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; -import static org.odk.collect.android.formentry.questions.WidgetViewUtils.createSimpleButton; import static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes; import android.annotation.SuppressLint; @@ -26,19 +25,16 @@ import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.view.View; -import android.widget.Button; - -import org.odk.collect.android.R; +import org.javarosa.form.api.FormEntryPrompt; +import org.odk.collect.android.databinding.AnnotateWidgetBinding; import org.odk.collect.draw.DrawActivity; import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.utilities.Appearances; import org.odk.collect.android.utilities.FileUtils; import org.odk.collect.android.utilities.QuestionMediaManager; -import org.odk.collect.android.widgets.interfaces.ButtonClickListener; import org.odk.collect.android.widgets.utilities.ImageCaptureIntentCreator; import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry; import org.odk.collect.androidshared.ui.ToastUtils; - import java.io.File; import java.util.Locale; @@ -50,47 +46,60 @@ * @author Yaw Anokwa (yanokwa@gmail.com) */ @SuppressLint("ViewConstructor") -public class AnnotateWidget extends BaseImageWidget implements ButtonClickListener { - - Button captureButton; - Button chooseButton; - Button annotateButton; +public class AnnotateWidget extends BaseImageWidget { + AnnotateWidgetBinding binding; public AnnotateWidget(Context context, QuestionDetails prompt, QuestionMediaManager questionMediaManager, WaitingForDataRegistry waitingForDataRegistry, String tmpImageFilePath) { super(context, prompt, questionMediaManager, waitingForDataRegistry, tmpImageFilePath); - render(); - imageClickHandler = new DrawImageClickHandler(DrawActivity.OPTION_ANNOTATE, RequestCodes.ANNOTATE_IMAGE, org.odk.collect.strings.R.string.annotate_image); imageCaptureHandler = new ImageCaptureHandler(); - setUpLayout(); - updateAnswer(); - adjustAnnotateButtonAvailability(); - addAnswerView(answerLayout); + + render(); } @Override - protected void setUpLayout() { - super.setUpLayout(); - captureButton = createSimpleButton(getContext(), questionDetails.isReadOnly(), getContext().getString(org.odk.collect.strings.R.string.capture_image), this, false, R.id.capture_image); - - chooseButton = createSimpleButton(getContext(), questionDetails.isReadOnly(), getContext().getString(org.odk.collect.strings.R.string.choose_image), this, true, R.id.choose_image); + protected View onCreateAnswerView(Context context, FormEntryPrompt prompt, int answerFontSize) { + binding = AnnotateWidgetBinding.inflate(((Activity) context).getLayoutInflater()); + errorTextView = binding.errorMessage; + imageView = binding.image; + updateAnswer(); - annotateButton = createSimpleButton(getContext(), questionDetails.isReadOnly(), getContext().getString(org.odk.collect.strings.R.string.markup_image), this, true, R.id.markup_image); + if (getFormEntryPrompt().getAppearanceHint() != null && getFormEntryPrompt().getAppearanceHint().toLowerCase(Locale.ENGLISH).contains(Appearances.NEW)) { + binding.chooseButton.setVisibility(View.GONE); + } - annotateButton.setOnClickListener(v -> imageClickHandler.clickImage("annotateButton")); + if (binaryName == null || binding.image.getVisibility() == GONE) { + binding.annotateButton.setEnabled(false); + } - answerLayout.addView(captureButton); - answerLayout.addView(chooseButton); - answerLayout.addView(annotateButton); - answerLayout.addView(errorTextView); - answerLayout.addView(imageView); + binding.captureButton.setOnClickListener(v -> getPermissionsProvider().requestCameraPermission((Activity) getContext(), () -> { + Intent intent = ImageCaptureIntentCreator.imageCaptureIntent(getFormEntryPrompt(), getContext(), tmpImageFilePath); + imageCaptureHandler.captureImage(intent, RequestCodes.IMAGE_CAPTURE, org.odk.collect.strings.R.string.annotate_image); + })); + binding.chooseButton.setOnClickListener(v -> imageCaptureHandler.chooseImage(org.odk.collect.strings.R.string.annotate_image)); + binding.annotateButton.setOnClickListener(v -> imageClickHandler.clickImage("annotateButton")); + binding.image.setOnClickListener(v -> imageClickHandler.clickImage("viewImage")); + + if (questionDetails.isReadOnly()) { + binding.captureButton.setVisibility(View.GONE); + binding.chooseButton.setVisibility(View.GONE); + binding.annotateButton.setVisibility(View.GONE); + } - hideButtonsIfNeeded(); + return binding.getRoot(); } @Override public Intent addExtrasToIntent(Intent intent) { - intent.putExtra(DrawActivity.SCREEN_ORIENTATION, calculateScreenOrientation()); + Bitmap bmp = null; + if (binding.image.getDrawable() != null) { + bmp = ((BitmapDrawable) binding.image.getDrawable()).getBitmap(); + } + + int screenOrientation = bmp != null && bmp.getHeight() > bmp.getWidth() ? + SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_LANDSCAPE; + + intent.putExtra(DrawActivity.SCREEN_ORIENTATION, screenOrientation); return intent; } @@ -102,63 +111,24 @@ protected boolean doesSupportDefaultValues() { @Override public void clearAnswer() { super.clearAnswer(); - annotateButton.setEnabled(false); - - // reset buttons - captureButton.setText(getContext().getString(org.odk.collect.strings.R.string.capture_image)); + binding.annotateButton.setEnabled(false); + binding.captureButton.setText(getContext().getString(org.odk.collect.strings.R.string.capture_image)); } @Override public void setOnLongClickListener(OnLongClickListener l) { - captureButton.setOnLongClickListener(l); - chooseButton.setOnLongClickListener(l); - annotateButton.setOnLongClickListener(l); + binding.captureButton.setOnLongClickListener(l); + binding.chooseButton.setOnLongClickListener(l); + binding.annotateButton.setOnLongClickListener(l); super.setOnLongClickListener(l); } @Override public void cancelLongPress() { super.cancelLongPress(); - captureButton.cancelLongPress(); - chooseButton.cancelLongPress(); - annotateButton.cancelLongPress(); - } - - @Override - public void onButtonClick(int buttonId) { - if (buttonId == R.id.capture_image) { - getPermissionsProvider().requestCameraPermission((Activity) getContext(), this::captureImage); - } else if (buttonId == R.id.choose_image) { - imageCaptureHandler.chooseImage(org.odk.collect.strings.R.string.annotate_image); - } - } - - private void adjustAnnotateButtonAvailability() { - if (binaryName == null || imageView.getVisibility() == GONE) { - annotateButton.setEnabled(false); - } - } - - private void hideButtonsIfNeeded() { - if (getFormEntryPrompt().getAppearanceHint() != null - && getFormEntryPrompt().getAppearanceHint().toLowerCase(Locale.ENGLISH).contains(Appearances.NEW)) { - chooseButton.setVisibility(View.GONE); - } - } - - private int calculateScreenOrientation() { - Bitmap bmp = null; - if (imageView.getDrawable() != null) { - bmp = ((BitmapDrawable) imageView.getDrawable()).getBitmap(); - } - - return bmp != null && bmp.getHeight() > bmp.getWidth() ? - SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_LANDSCAPE; - } - - private void captureImage() { - Intent intent = ImageCaptureIntentCreator.imageCaptureIntent(getFormEntryPrompt(), getContext(), tmpImageFilePath); - imageCaptureHandler.captureImage(intent, RequestCodes.IMAGE_CAPTURE, org.odk.collect.strings.R.string.annotate_image); + binding.captureButton.cancelLongPress(); + binding.chooseButton.cancelLongPress(); + binding.annotateButton.cancelLongPress(); } @Override @@ -169,7 +139,7 @@ public void setData(Object newImageObj) { ToastUtils.showLongToast(getContext(), org.odk.collect.strings.R.string.gif_not_supported); } else { super.setData(newImageObj); - annotateButton.setEnabled(binaryName != null); + binding.annotateButton.setEnabled(binaryName != null); } } } diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/BaseImageWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/BaseImageWidget.java index 53e7362ce76..434d77ce6f3 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/BaseImageWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/BaseImageWidget.java @@ -16,8 +16,6 @@ package org.odk.collect.android.widgets; -import static org.odk.collect.android.formentry.questions.WidgetViewUtils.createAnswerImageView; - import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; @@ -69,6 +67,8 @@ public BaseImageWidget(Context context, QuestionDetails prompt, QuestionMediaMan this.questionMediaManager = questionMediaManager; this.waitingForDataRegistry = waitingForDataRegistry; this.tmpImageFilePath = tmpImageFilePath; + + binaryName = getFormEntryPrompt().getAnswerText(); } @Override @@ -150,24 +150,6 @@ public void onLoadSucceeded() { } } - protected void setUpLayout() { - errorTextView = new TextView(getContext()); - errorTextView.setId(View.generateViewId()); - errorTextView.setText(org.odk.collect.strings.R.string.selected_invalid_image); - - answerLayout = new LinearLayout(getContext()); - answerLayout.setOrientation(LinearLayout.VERTICAL); - - binaryName = getFormEntryPrompt().getAnswerText(); - - imageView = createAnswerImageView(getContext()); - imageView.setOnClickListener(v -> { - if (imageClickHandler != null) { - imageClickHandler.clickImage("viewImage"); - } - }); - } - /** * Enables a subclass to add extras to the intent before launching the draw activity. * diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/DrawWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/DrawWidget.java index 0263b172cfa..e6055b7ab51 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/DrawWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/DrawWidget.java @@ -15,17 +15,17 @@ package org.odk.collect.android.widgets; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.widget.Button; - +import android.view.View; +import org.javarosa.form.api.FormEntryPrompt; +import org.odk.collect.android.databinding.DrawWidgetBinding; import org.odk.collect.draw.DrawActivity; import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.utilities.QuestionMediaManager; -import org.odk.collect.android.widgets.interfaces.ButtonClickListener; import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry; -import static org.odk.collect.android.formentry.questions.WidgetViewUtils.createSimpleButton; import static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes; /** @@ -34,28 +34,31 @@ * @author BehrAtherton@gmail.com */ @SuppressLint("ViewConstructor") -public class DrawWidget extends BaseImageWidget implements ButtonClickListener { - - Button drawButton; +public class DrawWidget extends BaseImageWidget { + DrawWidgetBinding binding; public DrawWidget(Context context, QuestionDetails prompt, QuestionMediaManager questionMediaManager, WaitingForDataRegistry waitingForDataRegistry, String tmpImageFilePath) { super(context, prompt, questionMediaManager, waitingForDataRegistry, tmpImageFilePath); - render(); - imageClickHandler = new DrawImageClickHandler(DrawActivity.OPTION_DRAW, RequestCodes.DRAW_IMAGE, org.odk.collect.strings.R.string.draw_image); - setUpLayout(); + + render(); updateAnswer(); - addAnswerView(answerLayout); } @Override - protected void setUpLayout() { - super.setUpLayout(); - drawButton = createSimpleButton(getContext(), questionDetails.isReadOnly(), getContext().getString(org.odk.collect.strings.R.string.draw_image), this, false); + protected View onCreateAnswerView(Context context, FormEntryPrompt prompt, int answerFontSize) { + binding = DrawWidgetBinding.inflate(((Activity) context).getLayoutInflater()); + binding.drawButton.setOnClickListener(v -> imageClickHandler.clickImage("drawButton")); + binding.image.setOnClickListener(v -> imageClickHandler.clickImage("viewImage")); - answerLayout.addView(drawButton); - answerLayout.addView(errorTextView); - answerLayout.addView(imageView); + if (questionDetails.isReadOnly()) { + binding.drawButton.setVisibility(View.GONE); + } + + errorTextView = binding.errorMessage; + imageView = binding.image; + + return binding.getRoot(); } @Override @@ -71,24 +74,18 @@ protected boolean doesSupportDefaultValues() { @Override public void clearAnswer() { super.clearAnswer(); - // reset buttons - drawButton.setText(getContext().getString(org.odk.collect.strings.R.string.draw_image)); + binding.drawButton.setText(getContext().getString(org.odk.collect.strings.R.string.draw_image)); } @Override public void setOnLongClickListener(OnLongClickListener l) { - drawButton.setOnLongClickListener(l); + binding.drawButton.setOnLongClickListener(l); super.setOnLongClickListener(l); } @Override public void cancelLongPress() { super.cancelLongPress(); - drawButton.cancelLongPress(); - } - - @Override - public void onButtonClick(int buttonId) { - imageClickHandler.clickImage("drawButton"); + binding.drawButton.cancelLongPress(); } } diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/ImageWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/ImageWidget.java index d9d76996a7e..d5797786ae5 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/ImageWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/ImageWidget.java @@ -14,7 +14,6 @@ package org.odk.collect.android.widgets; -import static org.odk.collect.android.formentry.questions.WidgetViewUtils.createSimpleButton; import static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes; import android.annotation.SuppressLint; @@ -22,20 +21,17 @@ import android.content.Context; import android.content.Intent; import android.view.View; -import android.widget.Button; - -import org.odk.collect.android.R; +import org.javarosa.form.api.FormEntryPrompt; +import org.odk.collect.android.databinding.ImageWidgetBinding; import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.storage.StoragePathProvider; import org.odk.collect.android.storage.StorageSubdirectory; import org.odk.collect.android.utilities.Appearances; import org.odk.collect.android.utilities.QuestionMediaManager; -import org.odk.collect.android.widgets.interfaces.ButtonClickListener; import org.odk.collect.android.widgets.utilities.ImageCaptureIntentCreator; import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry; import org.odk.collect.androidshared.system.CameraUtils; import org.odk.collect.selfiecamera.CaptureSelfieActivity; - import java.util.Locale; /** @@ -46,41 +42,43 @@ */ @SuppressLint("ViewConstructor") -public class ImageWidget extends BaseImageWidget implements ButtonClickListener { - - Button captureButton; - Button chooseButton; +public class ImageWidget extends BaseImageWidget { + ImageWidgetBinding binding; private boolean selfie; public ImageWidget(Context context, final QuestionDetails prompt, QuestionMediaManager questionMediaManager, WaitingForDataRegistry waitingForDataRegistry, String tmpImageFilePath) { super(context, prompt, questionMediaManager, waitingForDataRegistry, tmpImageFilePath); - render(); - imageClickHandler = new ViewImageClickHandler(); imageCaptureHandler = new ImageCaptureHandler(); - setUpLayout(); + + render(); updateAnswer(); - addAnswerView(answerLayout); } @Override - protected void setUpLayout() { - super.setUpLayout(); + protected View onCreateAnswerView(Context context, FormEntryPrompt prompt, int answerFontSize) { + binding = ImageWidgetBinding.inflate(((Activity) context).getLayoutInflater()); - String appearance = getFormEntryPrompt().getAppearanceHint(); - selfie = Appearances.isFrontCameraAppearance(getFormEntryPrompt()); + String appearance = prompt.getAppearanceHint(); + selfie = Appearances.isFrontCameraAppearance(prompt); + if (selfie || ((appearance != null && appearance.toLowerCase(Locale.ENGLISH).contains(Appearances.NEW)))) { + binding.chooseButton.setVisibility(View.GONE); + } - captureButton = createSimpleButton(getContext(), questionDetails.isReadOnly(), getContext().getString(org.odk.collect.strings.R.string.capture_image), this, false, R.id.capture_image); + binding.captureButton.setOnClickListener(v -> getPermissionsProvider().requestCameraPermission((Activity) getContext(), this::captureImage)); + binding.chooseButton.setOnClickListener(v -> imageCaptureHandler.chooseImage(org.odk.collect.strings.R.string.choose_image)); + binding.image.setOnClickListener(v -> imageClickHandler.clickImage("viewImage")); - chooseButton = createSimpleButton(getContext(), questionDetails.isReadOnly(), getContext().getString(org.odk.collect.strings.R.string.choose_image), this, true, R.id.choose_image); + if (questionDetails.isReadOnly()) { + binding.captureButton.setVisibility(View.GONE); + binding.chooseButton.setVisibility(View.GONE); + } - answerLayout.addView(captureButton); - answerLayout.addView(chooseButton); - answerLayout.addView(errorTextView); - answerLayout.addView(imageView); + errorTextView = binding.errorMessage; + imageView = binding.image; - hideButtonsIfNeeded(appearance); + return binding.getRoot(); } @Override @@ -96,38 +94,21 @@ protected boolean doesSupportDefaultValues() { @Override public void clearAnswer() { super.clearAnswer(); - // reset buttons - captureButton.setText(getContext().getString(org.odk.collect.strings.R.string.capture_image)); + binding.captureButton.setText(getContext().getString(org.odk.collect.strings.R.string.capture_image)); } @Override public void setOnLongClickListener(OnLongClickListener l) { - captureButton.setOnLongClickListener(l); - chooseButton.setOnLongClickListener(l); + binding.captureButton.setOnLongClickListener(l); + binding.chooseButton.setOnLongClickListener(l); super.setOnLongClickListener(l); } @Override public void cancelLongPress() { super.cancelLongPress(); - captureButton.cancelLongPress(); - chooseButton.cancelLongPress(); - } - - @Override - public void onButtonClick(int buttonId) { - if (buttonId == R.id.capture_image) { - getPermissionsProvider().requestCameraPermission((Activity) getContext(), this::captureImage); - } else if (buttonId == R.id.choose_image) { - imageCaptureHandler.chooseImage(org.odk.collect.strings.R.string.choose_image); - } - } - - private void hideButtonsIfNeeded(String appearance) { - if (selfie || ((appearance != null - && appearance.toLowerCase(Locale.ENGLISH).contains(Appearances.NEW)))) { - chooseButton.setVisibility(View.GONE); - } + binding.captureButton.cancelLongPress(); + binding.chooseButton.cancelLongPress(); } private void captureImage() { @@ -140,5 +121,4 @@ private void captureImage() { imageCaptureHandler.captureImage(intent, RequestCodes.IMAGE_CAPTURE, org.odk.collect.strings.R.string.capture_image); } } - } diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/SignatureWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/SignatureWidget.java index 17b4852848d..efd7e4eb159 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/SignatureWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/SignatureWidget.java @@ -14,18 +14,18 @@ package org.odk.collect.android.widgets; -import static org.odk.collect.android.formentry.questions.WidgetViewUtils.createSimpleButton; import static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.widget.Button; - +import android.view.View; +import org.javarosa.form.api.FormEntryPrompt; +import org.odk.collect.android.databinding.SignatureWidgetBinding; import org.odk.collect.draw.DrawActivity; import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.utilities.QuestionMediaManager; -import org.odk.collect.android.widgets.interfaces.ButtonClickListener; import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry; /** @@ -34,28 +34,31 @@ * @author BehrAtherton@gmail.com */ @SuppressLint("ViewConstructor") -public class SignatureWidget extends BaseImageWidget implements ButtonClickListener { - - Button signButton; +public class SignatureWidget extends BaseImageWidget { + SignatureWidgetBinding binding; public SignatureWidget(Context context, QuestionDetails prompt, QuestionMediaManager questionMediaManager, WaitingForDataRegistry waitingForDataRegistry, String tmpImageFilePath) { super(context, prompt, questionMediaManager, waitingForDataRegistry, tmpImageFilePath); - render(); - imageClickHandler = new DrawImageClickHandler(DrawActivity.OPTION_SIGNATURE, RequestCodes.SIGNATURE_CAPTURE, org.odk.collect.strings.R.string.signature_capture); - setUpLayout(); + + render(); updateAnswer(); - addAnswerView(answerLayout); } @Override - protected void setUpLayout() { - super.setUpLayout(); - signButton = createSimpleButton(getContext(), questionDetails.isReadOnly(), getContext().getString(org.odk.collect.strings.R.string.sign_button), this, false); + protected View onCreateAnswerView(Context context, FormEntryPrompt prompt, int answerFontSize) { + binding = SignatureWidgetBinding.inflate(((Activity) context).getLayoutInflater()); + binding.signButton.setOnClickListener(v -> imageClickHandler.clickImage("signButton")); + binding.image.setOnClickListener(v -> imageClickHandler.clickImage("viewImage")); - answerLayout.addView(signButton); - answerLayout.addView(errorTextView); - answerLayout.addView(imageView); + if (questionDetails.isReadOnly()) { + binding.signButton.setVisibility(View.GONE); + } + + errorTextView = binding.errorMessage; + imageView = binding.image; + + return binding.getRoot(); } @Override @@ -71,24 +74,18 @@ protected boolean doesSupportDefaultValues() { @Override public void clearAnswer() { super.clearAnswer(); - // reset buttons - signButton.setText(getContext().getString(org.odk.collect.strings.R.string.sign_button)); + binding.signButton.setText(getContext().getString(org.odk.collect.strings.R.string.sign_button)); } @Override public void setOnLongClickListener(OnLongClickListener l) { - signButton.setOnLongClickListener(l); + binding.signButton.setOnLongClickListener(l); super.setOnLongClickListener(l); } @Override public void cancelLongPress() { super.cancelLongPress(); - signButton.cancelLongPress(); - } - - @Override - public void onButtonClick(int buttonId) { - imageClickHandler.clickImage("signButton"); + binding.signButton.cancelLongPress(); } } diff --git a/collect_app/src/main/res/layout/annotate_widget.xml b/collect_app/src/main/res/layout/annotate_widget.xml new file mode 100644 index 00000000000..46397d27ae5 --- /dev/null +++ b/collect_app/src/main/res/layout/annotate_widget.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/collect_app/src/main/res/layout/draw_widget.xml b/collect_app/src/main/res/layout/draw_widget.xml new file mode 100644 index 00000000000..6d89e509eb0 --- /dev/null +++ b/collect_app/src/main/res/layout/draw_widget.xml @@ -0,0 +1,40 @@ + + + + + + + + + \ No newline at end of file diff --git a/collect_app/src/main/res/layout/image_widget.xml b/collect_app/src/main/res/layout/image_widget.xml new file mode 100644 index 00000000000..7945fd0a304 --- /dev/null +++ b/collect_app/src/main/res/layout/image_widget.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/collect_app/src/main/res/layout/signature_widget.xml b/collect_app/src/main/res/layout/signature_widget.xml new file mode 100644 index 00000000000..f2fd4d1693b --- /dev/null +++ b/collect_app/src/main/res/layout/signature_widget.xml @@ -0,0 +1,40 @@ + + + + + + + + + \ No newline at end of file diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/AnnotateWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/AnnotateWidgetTest.java index d586f195a47..646ef1052ff 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/AnnotateWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/AnnotateWidgetTest.java @@ -89,14 +89,14 @@ public Object createBinaryData(@NotNull StringData answerData) { public void buttonsShouldLaunchCorrectIntentsWhenThereIsNoCustomPackage() { stubAllRuntimePermissionsGranted(true); - Intent intent = getIntentLaunchedByClick(R.id.capture_image); + Intent intent = getIntentLaunchedByClick(R.id.capture_button); assertActionEquals(MediaStore.ACTION_IMAGE_CAPTURE, intent); assertThat(intent.getPackage(), equalTo(null)); - intent = getIntentLaunchedByClick(R.id.choose_image); + intent = getIntentLaunchedByClick(R.id.choose_button); assertActionEquals(Intent.ACTION_GET_CONTENT, intent); - intent = getIntentLaunchedByClick(R.id.markup_image); + intent = getIntentLaunchedByClick(R.id.annotate_button); assertComponentEquals(activity, DrawActivity.class, intent); assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_ANNOTATE, intent); } @@ -109,11 +109,11 @@ public void buttonsShouldLaunchCorrectIntentsWhenCustomPackageIsSet() { stubAllRuntimePermissionsGranted(true); - Intent intent = getIntentLaunchedByClick(R.id.capture_image); + Intent intent = getIntentLaunchedByClick(R.id.capture_button); assertActionEquals(MediaStore.ACTION_IMAGE_CAPTURE, intent); assertThat(intent.getPackage(), equalTo("com.customcameraapp")); - intent = getIntentLaunchedByClick(R.id.choose_image); + intent = getIntentLaunchedByClick(R.id.choose_button); assertActionEquals(Intent.ACTION_GET_CONTENT, intent); assertTypeEquals("image/*", intent); } @@ -122,16 +122,16 @@ public void buttonsShouldLaunchCorrectIntentsWhenCustomPackageIsSet() { public void buttonsShouldNotLaunchIntentsWhenPermissionsDenied() { stubAllRuntimePermissionsGranted(false); - assertNull(getIntentLaunchedByClick(R.id.capture_image)); + assertNull(getIntentLaunchedByClick(R.id.capture_button)); } @Test public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() { when(formEntryPrompt.isReadOnly()).thenReturn(true); - assertThat(getSpyWidget().captureButton.getVisibility(), is(View.GONE)); - assertThat(getSpyWidget().chooseButton.getVisibility(), is(View.GONE)); - assertThat(getSpyWidget().annotateButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.captureButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.chooseButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.annotateButton.getVisibility(), is(View.GONE)); } @Test @@ -139,9 +139,9 @@ public void whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisable readOnlyOverride = true; when(formEntryPrompt.isReadOnly()).thenReturn(false); - assertThat(getSpyWidget().captureButton.getVisibility(), is(View.GONE)); - assertThat(getSpyWidget().chooseButton.getVisibility(), is(View.GONE)); - assertThat(getSpyWidget().annotateButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.captureButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.chooseButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.annotateButton.getVisibility(), is(View.GONE)); } @Test @@ -262,13 +262,13 @@ public void markupButtonShouldBeDisabledIfImageAbsent() throws Exception { .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER) .build(); - assertThat(getWidget().annotateButton.isEnabled(), is(false)); + assertThat(getWidget().binding.annotateButton.isEnabled(), is(false)); formEntryPrompt = new MockFormEntryPromptBuilder() .withAnswerDisplayText(DrawWidgetTest.USER_SPECIFIED_IMAGE_ANSWER) .build(); - assertThat(getWidget().annotateButton.isEnabled(), is(false)); + assertThat(getWidget().binding.annotateButton.isEnabled(), is(false)); } @Test @@ -295,7 +295,7 @@ public ImageLoader providesImageLoader() { .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER) .build(); - Intent intent = getIntentLaunchedByClick(R.id.markup_image); + Intent intent = getIntentLaunchedByClick(R.id.annotate_button); assertComponentEquals(activity, DrawActivity.class, intent); assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_ANNOTATE, intent); assertExtraEquals(DrawActivity.REF_IMAGE, Uri.fromFile(file), intent); @@ -317,7 +317,7 @@ public ReferenceManager providesReferenceManager() { .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER) .build(); - Intent intent = getIntentLaunchedByClick(R.id.markup_image); + Intent intent = getIntentLaunchedByClick(R.id.annotate_button); assertComponentEquals(activity, DrawActivity.class, intent); assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_ANNOTATE, intent); assertThat(intent.hasExtra(DrawActivity.REF_IMAGE), is(false)); @@ -325,7 +325,7 @@ public ReferenceManager providesReferenceManager() { @Test public void whenThereIsNoAnswer_doNotPassUriToDrawActivity() { - Intent intent = getIntentLaunchedByClick(R.id.markup_image); + Intent intent = getIntentLaunchedByClick(R.id.annotate_button); assertComponentEquals(activity, DrawActivity.class, intent); assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_ANNOTATE, intent); assertThat(intent.hasExtra(DrawActivity.REF_IMAGE), is(false)); diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/DrawWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/DrawWidgetTest.java index bc99feb72b9..b0ec2ea67af 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/DrawWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/DrawWidgetTest.java @@ -85,7 +85,7 @@ public StringData getNextAnswer() { public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() { when(formEntryPrompt.isReadOnly()).thenReturn(true); - assertThat(getSpyWidget().drawButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.drawButton.getVisibility(), is(View.GONE)); } @Test @@ -93,7 +93,7 @@ public void whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisable readOnlyOverride = true; when(formEntryPrompt.isReadOnly()).thenReturn(false); - assertThat(getSpyWidget().drawButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.drawButton.getVisibility(), is(View.GONE)); } @Test @@ -212,7 +212,7 @@ public ImageLoader providesImageLoader() { .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER) .build(); - Intent intent = getIntentLaunchedByClick(R.id.simple_button); + Intent intent = getIntentLaunchedByClick(R.id.draw_button); assertComponentEquals(activity, DrawActivity.class, intent); assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_DRAW, intent); assertExtraEquals(DrawActivity.REF_IMAGE, Uri.fromFile(file), intent); @@ -234,7 +234,7 @@ public ReferenceManager providesReferenceManager() { .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER) .build(); - Intent intent = getIntentLaunchedByClick(R.id.simple_button); + Intent intent = getIntentLaunchedByClick(R.id.draw_button); assertComponentEquals(activity, DrawActivity.class, intent); assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_DRAW, intent); assertThat(intent.hasExtra(DrawActivity.REF_IMAGE), is(false)); @@ -242,7 +242,7 @@ public ReferenceManager providesReferenceManager() { @Test public void whenThereIsNoAnswer_doNotPassUriToDrawActivity() { - Intent intent = getIntentLaunchedByClick(R.id.simple_button); + Intent intent = getIntentLaunchedByClick(R.id.draw_button); assertComponentEquals(activity, DrawActivity.class, intent); assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_DRAW, intent); assertThat(intent.hasExtra(DrawActivity.REF_IMAGE), is(false)); diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/ImageWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/ImageWidgetTest.java index fceb8519521..f6487ef1dda 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/ImageWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/ImageWidgetTest.java @@ -78,11 +78,11 @@ public StringData getNextAnswer() { public void buttonsShouldLaunchCorrectIntentsWhenThereIsNoCustomPackage() { stubAllRuntimePermissionsGranted(true); - Intent intent = getIntentLaunchedByClick(R.id.capture_image); + Intent intent = getIntentLaunchedByClick(R.id.capture_button); assertActionEquals(MediaStore.ACTION_IMAGE_CAPTURE, intent); assertThat(intent.getPackage(), equalTo(null)); - intent = getIntentLaunchedByClick(R.id.choose_image); + intent = getIntentLaunchedByClick(R.id.choose_button); assertActionEquals(Intent.ACTION_GET_CONTENT, intent); assertTypeEquals("image/*", intent); } @@ -95,11 +95,11 @@ public void buttonsShouldLaunchCorrectIntentsWhenCustomPackageIsSet() { stubAllRuntimePermissionsGranted(true); - Intent intent = getIntentLaunchedByClick(R.id.capture_image); + Intent intent = getIntentLaunchedByClick(R.id.capture_button); assertActionEquals(MediaStore.ACTION_IMAGE_CAPTURE, intent); assertThat(intent.getPackage(), equalTo("com.customcameraapp")); - intent = getIntentLaunchedByClick(R.id.choose_image); + intent = getIntentLaunchedByClick(R.id.choose_button); assertActionEquals(Intent.ACTION_GET_CONTENT, intent); assertTypeEquals("image/*", intent); } @@ -108,15 +108,15 @@ public void buttonsShouldLaunchCorrectIntentsWhenCustomPackageIsSet() { public void buttonsShouldNotLaunchIntentsWhenPermissionsDenied() { stubAllRuntimePermissionsGranted(false); - assertNull(getIntentLaunchedByClick(R.id.capture_image)); + assertNull(getIntentLaunchedByClick(R.id.capture_button)); } @Test public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() { when(formEntryPrompt.isReadOnly()).thenReturn(true); - assertThat(getSpyWidget().captureButton.getVisibility(), is(View.GONE)); - assertThat(getSpyWidget().chooseButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.captureButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.chooseButton.getVisibility(), is(View.GONE)); } @Test @@ -124,8 +124,8 @@ public void whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisable readOnlyOverride = true; when(formEntryPrompt.isReadOnly()).thenReturn(false); - assertThat(getSpyWidget().captureButton.getVisibility(), is(View.GONE)); - assertThat(getSpyWidget().chooseButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.captureButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.chooseButton.getVisibility(), is(View.GONE)); } @Test diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/SignatureWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/SignatureWidgetTest.java index ab3e0ac8df1..4532f081033 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/SignatureWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/SignatureWidgetTest.java @@ -78,7 +78,7 @@ public StringData getNextAnswer() { public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() { when(formEntryPrompt.isReadOnly()).thenReturn(true); - assertThat(getSpyWidget().signButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.signButton.getVisibility(), is(View.GONE)); } @Test @@ -86,7 +86,7 @@ public void whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisable readOnlyOverride = true; when(formEntryPrompt.isReadOnly()).thenReturn(false); - assertThat(getSpyWidget().signButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.signButton.getVisibility(), is(View.GONE)); } @Test @@ -206,7 +206,7 @@ public ImageLoader providesImageLoader() { .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER) .build(); - Intent intent = getIntentLaunchedByClick(R.id.simple_button); + Intent intent = getIntentLaunchedByClick(R.id.sign_button); assertComponentEquals(activity, DrawActivity.class, intent); assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_SIGNATURE, intent); assertExtraEquals(DrawActivity.REF_IMAGE, Uri.fromFile(file), intent); @@ -228,7 +228,7 @@ public ReferenceManager providesReferenceManager() { .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER) .build(); - Intent intent = getIntentLaunchedByClick(R.id.simple_button); + Intent intent = getIntentLaunchedByClick(R.id.sign_button); assertComponentEquals(activity, DrawActivity.class, intent); assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_SIGNATURE, intent); assertThat(intent.hasExtra(DrawActivity.REF_IMAGE), is(false)); @@ -236,7 +236,7 @@ public ReferenceManager providesReferenceManager() { @Test public void whenThereIsNoAnswer_doNotPassUriToDrawActivity() { - Intent intent = getIntentLaunchedByClick(R.id.simple_button); + Intent intent = getIntentLaunchedByClick(R.id.sign_button); assertComponentEquals(activity, DrawActivity.class, intent); assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_SIGNATURE, intent); assertThat(intent.hasExtra(DrawActivity.REF_IMAGE), is(false)); From c1830b8d5237536b4e5f50f44c698b33eef1b0dc Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Sat, 23 Dec 2023 22:41:38 +0100 Subject: [PATCH 006/750] Reworked the RankingWidget --- .../android/support/pages/FormEntryPage.java | 2 +- .../formentry/questions/WidgetViewUtils.kt | 14 --- .../android/widgets/items/RankingWidget.java | 95 +++++++++---------- .../src/main/res/layout/ranking_widget.xml | 28 ++++++ .../widgets/items/RankingWidgetTest.java | 2 +- 5 files changed, 73 insertions(+), 68 deletions(-) create mode 100644 collect_app/src/main/res/layout/ranking_widget.xml diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java index 90374c07cf3..ef4b95b5c56 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java @@ -184,7 +184,7 @@ public FormEntryPage clickWidgetButton() { } public FormEntryPage clickRankingButton() { - onView(withId(R.id.simple_button)).perform(click()); + onView(withId(R.id.rank_items_button)).perform(click()); return this; } diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/questions/WidgetViewUtils.kt b/collect_app/src/main/java/org/odk/collect/android/formentry/questions/WidgetViewUtils.kt index d8f56d9b2d7..1eed9b8ef07 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/questions/WidgetViewUtils.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/questions/WidgetViewUtils.kt @@ -1,32 +1,18 @@ package org.odk.collect.android.formentry.questions import android.content.Context -import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.widget.Button import android.widget.TableLayout -import android.widget.TextView import androidx.annotation.IdRes import com.google.android.material.button.MaterialButton import org.odk.collect.android.R -import org.odk.collect.android.utilities.ThemeUtils import org.odk.collect.android.widgets.QuestionWidget import org.odk.collect.android.widgets.interfaces.ButtonClickListener import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard.allowClick object WidgetViewUtils { - @JvmStatic - fun createAnswerTextView(context: Context, text: CharSequence?, answerFontSize: Int): TextView { - return TextView(context).apply { - id = R.id.answer_text - setTextColor(ThemeUtils(context).colorOnSurface) - setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize.toFloat()) - setPadding(20, 20, 20, 20) - setText(text) - } - } - @JvmStatic @JvmOverloads fun createSimpleButton(context: Context, readOnly: Boolean, text: String?, listener: ButtonClickListener, addMargin: Boolean, @IdRes withId: Int = R.id.simple_button): Button { diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/items/RankingWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/items/RankingWidget.java index afd7607b503..ce245e0e35d 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/items/RankingWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/items/RankingWidget.java @@ -16,51 +16,68 @@ package org.odk.collect.android.widgets.items; -import static org.odk.collect.android.formentry.questions.WidgetViewUtils.createAnswerTextView; -import static org.odk.collect.android.formentry.questions.WidgetViewUtils.createSimpleButton; - import android.annotation.SuppressLint; +import android.app.Activity; import android.content.Context; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.TextView; - +import android.util.TypedValue; +import android.view.View; +import androidx.annotation.NonNull; import org.javarosa.core.model.SelectChoice; import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.data.SelectMultiData; import org.javarosa.core.model.data.helper.Selection; +import org.javarosa.form.api.FormEntryPrompt; import org.odk.collect.android.activities.FormFillingActivity; +import org.odk.collect.android.databinding.RankingWidgetBinding; import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.fragments.dialogs.RankingWidgetDialog; import org.odk.collect.android.utilities.HtmlUtils; import org.odk.collect.android.widgets.QuestionWidget; -import org.odk.collect.android.widgets.interfaces.ButtonClickListener; import org.odk.collect.android.widgets.interfaces.SelectChoiceLoader; import org.odk.collect.android.widgets.interfaces.WidgetDataReceiver; -import org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils; import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry; import org.odk.collect.android.widgets.warnings.SpacesInUnderlyingValuesWarning; - import java.util.ArrayList; import java.util.List; @SuppressLint("ViewConstructor") -public class RankingWidget extends QuestionWidget implements WidgetDataReceiver, ButtonClickListener { +public class RankingWidget extends QuestionWidget implements WidgetDataReceiver { private final WaitingForDataRegistry waitingForDataRegistry; private List savedItems; - Button showRankingDialogButton; - private TextView answerTextView; private final List items; + RankingWidgetBinding binding; public RankingWidget(Context context, QuestionDetails prompt, WaitingForDataRegistry waitingForDataRegistry, SelectChoiceLoader selectChoiceLoader) { super(context, prompt); - render(); - this.waitingForDataRegistry = waitingForDataRegistry; items = ItemsWidgetUtils.loadItemsAndHandleErrors(this, questionDetails.getPrompt(), selectChoiceLoader); + readSavedItems(); + render(); + } + + @Override + protected View onCreateAnswerView(@NonNull Context context, @NonNull FormEntryPrompt prompt, int answerFontSize) { + binding = RankingWidgetBinding.inflate(((Activity) context).getLayoutInflater()); + + binding.rankItemsButton.setOnClickListener(v -> { + waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex()); + RankingWidgetDialog rankingWidgetDialog = new RankingWidgetDialog(savedItems == null ? items : savedItems, getFormEntryPrompt()); + rankingWidgetDialog.show(((FormFillingActivity) getContext()).getSupportFragmentManager(), "RankingDialog"); + }); + binding.answer.setText(getAnswerText()); + binding.answer.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize); + binding.answer.setVisibility(binding.answer.getText().toString().isBlank() ? GONE : VISIBLE); + + if (questionDetails.isReadOnly()) { + binding.rankItemsButton.setVisibility(View.GONE); + } + + SpacesInUnderlyingValuesWarning + .forQuestionWidget(this) + .renderWarningIfNecessary(savedItems == null ? items : savedItems); - setUpLayout(getOrderedItems()); + return binding.getRoot(); } @Override @@ -78,47 +95,39 @@ public IAnswerData getAnswer() { @Override public void clearAnswer() { savedItems = null; - answerTextView.setText(getAnswerText()); - answerTextView.setVisibility(GONE); + binding.answer.setText(null); + binding.answer.setVisibility(GONE); widgetValueChanged(); } @Override public void setOnLongClickListener(OnLongClickListener l) { - showRankingDialogButton.setOnLongClickListener(l); + binding.rankItemsButton.setOnLongClickListener(l); + binding.answer.setOnLongClickListener(l); } @Override public void cancelLongPress() { super.cancelLongPress(); - showRankingDialogButton.cancelLongPress(); + binding.rankItemsButton.cancelLongPress(); + binding.answer.cancelLongPress(); } @Override public void setData(Object values) { savedItems = (List) values; - answerTextView.setText(getAnswerText()); - answerTextView.setVisibility(answerTextView.getText().toString().isBlank() ? GONE : VISIBLE); + binding.answer.setText(getAnswerText()); + binding.answer.setVisibility(binding.answer.getText().toString().isBlank() ? GONE : VISIBLE); widgetValueChanged(); } - @Override - public void onButtonClick(int buttonId) { - waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex()); - - RankingWidgetDialog rankingWidgetDialog = new RankingWidgetDialog(savedItems == null ? items : savedItems, getFormEntryPrompt()); - rankingWidgetDialog.show(((FormFillingActivity) getContext()).getSupportFragmentManager(), "RankingDialog"); - } - - private List getOrderedItems() { + private void readSavedItems() { List savedOrderedItems = getFormEntryPrompt().getAnswerValue() == null ? new ArrayList<>() : (List) getFormEntryPrompt().getAnswerValue().getValue(); - if (savedOrderedItems.isEmpty()) { - return items; - } else { + if (!savedOrderedItems.isEmpty()) { savedItems = new ArrayList<>(); for (Selection selection : savedOrderedItems) { for (SelectChoice selectChoice : items) { @@ -134,27 +143,9 @@ private List getOrderedItems() { savedItems.add(selectChoice); } } - - return savedItems; } } - private void setUpLayout(List items) { - showRankingDialogButton = createSimpleButton(getContext(), getFormEntryPrompt().isReadOnly(), getContext().getString(org.odk.collect.strings.R.string.rank_items), this, false); - answerTextView = createAnswerTextView(getContext(), getAnswerText(), QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.HEADLINE_6)); - answerTextView.setVisibility(answerTextView.getText().toString().isBlank() ? GONE : VISIBLE); - - LinearLayout widgetLayout = new LinearLayout(getContext()); - widgetLayout.setOrientation(LinearLayout.VERTICAL); - widgetLayout.addView(showRankingDialogButton); - widgetLayout.addView(answerTextView); - - addAnswerView(widgetLayout); - SpacesInUnderlyingValuesWarning - .forQuestionWidget(this) - .renderWarningIfNecessary(items); - } - private CharSequence getAnswerText() { StringBuilder answerText = new StringBuilder(); if (savedItems != null) { diff --git a/collect_app/src/main/res/layout/ranking_widget.xml b/collect_app/src/main/res/layout/ranking_widget.xml new file mode 100644 index 00000000000..ff3b32e7029 --- /dev/null +++ b/collect_app/src/main/res/layout/ranking_widget.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/items/RankingWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/items/RankingWidgetTest.java index c420994b78f..b5f70a90950 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/items/RankingWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/items/RankingWidgetTest.java @@ -53,7 +53,7 @@ public MultipleItemsData getNextAnswer() { public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() { when(formEntryPrompt.isReadOnly()).thenReturn(true); - assertThat(getSpyWidget().showRankingDialogButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.rankItemsButton.getVisibility(), is(View.GONE)); } @Test From a99d0aa6aac1531bb552d1fadb362e7a2f16a3d3 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Sat, 23 Dec 2023 22:58:25 +0100 Subject: [PATCH 007/750] Reworked the VideoWidget --- .../feature/formentry/IntentGroupTest.java | 6 +- .../collect/android/widgets/VideoWidget.java | 93 +++++++------------ .../src/main/res/layout/video_widget.xml | 41 ++++++++ .../android/widgets/VideoWidgetTest.java | 28 +++--- 4 files changed, 92 insertions(+), 76 deletions(-) create mode 100644 collect_app/src/main/res/layout/video_widget.xml diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/IntentGroupTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/IntentGroupTest.java index 5147b0655af..d47e586c3eb 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/IntentGroupTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/IntentGroupTest.java @@ -218,7 +218,7 @@ private void assertAudioWidgetWithoutAnswer() { private void assertVideoWidgetWithoutAnswer() { onView(withText(is("Video external"))).perform(scrollTo()).check(matches(isDisplayed())); - onView(withId(R.id.play_video)).check(matches(not(isDisplayed()))); + onView(withId(R.id.play_video_button)).check(matches(not(isDisplayed()))); } private void assertFileWidgetWithoutAnswer() { @@ -236,8 +236,8 @@ private void assertAudioWidgetWithAnswer() { } private void assertVideoWidgetWithAnswer() { - onView(withId(R.id.play_video)).perform(scrollTo()).check(matches(isDisplayed())); - onView(withId(R.id.play_video)).check(matches(isEnabled())); + onView(withId(R.id.play_video_button)).perform(scrollTo()).check(matches(isDisplayed())); + onView(withId(R.id.play_video_button)).check(matches(isEnabled())); } private void assertFileWidgetWithAnswer() { diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/VideoWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/VideoWidget.java index 6a133c247a9..eb553578119 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/VideoWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/VideoWidget.java @@ -16,7 +16,6 @@ import static org.odk.collect.android.analytics.AnalyticsEvents.REQUEST_HIGH_RES_VIDEO; import static org.odk.collect.android.analytics.AnalyticsEvents.REQUEST_VIDEO_NOT_HIGH_RES; -import static org.odk.collect.android.formentry.questions.WidgetViewUtils.createSimpleButton; import static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes; import android.annotation.SuppressLint; @@ -26,18 +25,18 @@ import android.content.Intent; import android.provider.MediaStore; import android.view.View; -import android.widget.Button; -import android.widget.LinearLayout; import android.widget.Toast; +import androidx.annotation.NonNull; + import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.data.StringData; +import org.javarosa.form.api.FormEntryPrompt; import org.odk.collect.analytics.Analytics; -import org.odk.collect.android.R; +import org.odk.collect.android.databinding.VideoWidgetBinding; import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.utilities.Appearances; import org.odk.collect.android.utilities.QuestionMediaManager; -import org.odk.collect.android.widgets.interfaces.ButtonClickListener; import org.odk.collect.android.widgets.interfaces.FileWidget; import org.odk.collect.android.widgets.interfaces.WidgetDataReceiver; import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry; @@ -56,53 +55,50 @@ * @author Yaw Anokwa (yanokwa@gmail.com) */ @SuppressLint("ViewConstructor") -public class VideoWidget extends QuestionWidget implements FileWidget, ButtonClickListener, WidgetDataReceiver { +public class VideoWidget extends QuestionWidget implements FileWidget, WidgetDataReceiver { private final WaitingForDataRegistry waitingForDataRegistry; private final QuestionMediaManager questionMediaManager; - - Button captureButton; - Button playButton; - Button chooseButton; private String binaryName; + VideoWidgetBinding binding; public VideoWidget(Context context, QuestionDetails prompt, QuestionMediaManager questionMediaManager, WaitingForDataRegistry waitingForDataRegistry) { this(context, prompt, waitingForDataRegistry, questionMediaManager); - render(); } public VideoWidget(Context context, QuestionDetails questionDetails, WaitingForDataRegistry waitingForDataRegistry, QuestionMediaManager questionMediaManager) { super(context, questionDetails); - render(); - this.waitingForDataRegistry = waitingForDataRegistry; this.questionMediaManager = questionMediaManager; + binaryName = questionDetails.getPrompt().getAnswerText(); + render(); + } - captureButton = createSimpleButton(getContext(), questionDetails.isReadOnly(), getContext().getString(org.odk.collect.strings.R.string.capture_video), this, false, R.id.capture_video); - - chooseButton = createSimpleButton(getContext(), questionDetails.isReadOnly(), getContext().getString(org.odk.collect.strings.R.string.choose_video), this, true, R.id.choose_video); + @Override + protected View onCreateAnswerView(@NonNull Context context, @NonNull FormEntryPrompt prompt, int answerFontSize) { + binding = VideoWidgetBinding.inflate(((Activity) context).getLayoutInflater()); - playButton = createSimpleButton(getContext(), false, getContext().getString(org.odk.collect.strings.R.string.play_video), this, true, R.id.play_video); - playButton.setVisibility(VISIBLE); + binding.recordVideoButton.setOnClickListener(v -> getPermissionsProvider().requestCameraPermission((Activity) getContext(), this::captureVideo)); + binding.chooseVideoButton.setOnClickListener(v -> chooseVideo()); + binding.playVideoButton.setEnabled(binaryName != null); + binding.playVideoButton.setOnClickListener(v -> playVideoFile()); - // retrieve answer from data model and update ui - binaryName = questionDetails.getPrompt().getAnswerText(); - playButton.setEnabled(binaryName != null); + if (questionDetails.isReadOnly()) { + binding.recordVideoButton.setVisibility(View.GONE); + binding.chooseVideoButton.setVisibility(View.GONE); + } - // finish complex layout - LinearLayout answerLayout = new LinearLayout(getContext()); - answerLayout.setOrientation(LinearLayout.VERTICAL); - answerLayout.addView(captureButton); - answerLayout.addView(chooseButton); - answerLayout.addView(playButton); - addAnswerView(answerLayout); + if (getFormEntryPrompt().getAppearanceHint() != null + && getFormEntryPrompt().getAppearanceHint().toLowerCase(Locale.ENGLISH).contains(Appearances.NEW)) { + binding.chooseVideoButton.setVisibility(View.GONE); + } - hideButtonsIfNeeded(); + return binding.getRoot(); } @Override public void deleteFile() { questionMediaManager.deleteAnswerFile(getFormEntryPrompt().getIndex().toString(), - questionMediaManager.getAnswerFile(binaryName).getAbsolutePath()); + questionMediaManager.getAnswerFile(binaryName).getAbsolutePath()); binaryName = null; } @@ -110,10 +106,7 @@ public void deleteFile() { public void clearAnswer() { // remove the file deleteFile(); - - // reset buttons - playButton.setEnabled(false); - + binding.playVideoButton.setEnabled(false); widgetValueChanged(); } @@ -138,7 +131,7 @@ public void setData(Object object) { questionMediaManager.replaceAnswerFile(getFormEntryPrompt().getIndex().toString(), newVideo.getAbsolutePath()); binaryName = newVideo.getName(); widgetValueChanged(); - playButton.setEnabled(binaryName != null); + binding.playVideoButton.setEnabled(binaryName != null); } else { Timber.e(new Error("Inserting Video file FAILED")); } @@ -147,37 +140,19 @@ public void setData(Object object) { } } - private void hideButtonsIfNeeded() { - if (getFormEntryPrompt().getAppearanceHint() != null - && getFormEntryPrompt().getAppearanceHint().toLowerCase(Locale.ENGLISH).contains(Appearances.NEW)) { - chooseButton.setVisibility(View.GONE); - } - } - @Override public void setOnLongClickListener(OnLongClickListener l) { - captureButton.setOnLongClickListener(l); - chooseButton.setOnLongClickListener(l); - playButton.setOnLongClickListener(l); + binding.recordVideoButton.setOnLongClickListener(l); + binding.chooseVideoButton.setOnLongClickListener(l); + binding.playVideoButton.setOnLongClickListener(l); } @Override public void cancelLongPress() { super.cancelLongPress(); - captureButton.cancelLongPress(); - chooseButton.cancelLongPress(); - playButton.cancelLongPress(); - } - - @Override - public void onButtonClick(int id) { - if (id == R.id.capture_video) { - getPermissionsProvider().requestCameraPermission((Activity) getContext(), this::captureVideo); - } else if (id == R.id.choose_video) { - chooseVideo(); - } else if (id == R.id.play_video) { - playVideoFile(); - } + binding.recordVideoButton.cancelLongPress(); + binding.chooseVideoButton.cancelLongPress(); + binding.playVideoButton.cancelLongPress(); } private void captureVideo() { diff --git a/collect_app/src/main/res/layout/video_widget.xml b/collect_app/src/main/res/layout/video_widget.xml new file mode 100644 index 00000000000..e1895923754 --- /dev/null +++ b/collect_app/src/main/res/layout/video_widget.xml @@ -0,0 +1,41 @@ + + + + + + + + + \ No newline at end of file diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/VideoWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/VideoWidgetTest.java index b11205e6e96..c4024a4cf45 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/VideoWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/VideoWidgetTest.java @@ -66,15 +66,15 @@ public MediaUtils providesMediaUtils(IntentLauncher intentLauncher) { stubAllRuntimePermissionsGranted(true); - Intent intent = getIntentLaunchedByClick(R.id.capture_video); + Intent intent = getIntentLaunchedByClick(R.id.record_video_button); assertActionEquals(MediaStore.ACTION_VIDEO_CAPTURE, intent); - intent = getIntentLaunchedByClick(R.id.choose_video); + intent = getIntentLaunchedByClick(R.id.choose_video_button); assertActionEquals(Intent.ACTION_GET_CONTENT, intent); assertTypeEquals("video/*", intent); getWidget().setData(TempFiles.createTempFile(TempFiles.createTempDir())); - getIntentLaunchedByClick(R.id.play_video); + getIntentLaunchedByClick(R.id.play_video_button); verify(mediaUtils).openFile(any(), any(), any()); } @@ -82,18 +82,18 @@ public MediaUtils providesMediaUtils(IntentLauncher intentLauncher) { public void buttonsShouldNotLaunchIntentsWhenPermissionsDenied() { stubAllRuntimePermissionsGranted(false); - assertNull(getIntentLaunchedByClick(R.id.capture_video)); + assertNull(getIntentLaunchedByClick(R.id.record_video_button)); } @Test public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() { when(formEntryPrompt.isReadOnly()).thenReturn(true); - assertThat(getSpyWidget().captureButton.getVisibility(), is(View.GONE)); - assertThat(getSpyWidget().chooseButton.getVisibility(), is(View.GONE)); - assertThat(getSpyWidget().playButton.getVisibility(), is(View.VISIBLE)); - assertThat(getSpyWidget().playButton.isEnabled(), is(Boolean.FALSE)); - assertThat(getSpyWidget().playButton.getText(), is("Play Video")); + assertThat(getSpyWidget().binding.recordVideoButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.chooseVideoButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.playVideoButton.getVisibility(), is(View.VISIBLE)); + assertThat(getSpyWidget().binding.playVideoButton.isEnabled(), is(Boolean.FALSE)); + assertThat(getSpyWidget().binding.playVideoButton.getText(), is("Play Video")); } @Test @@ -101,10 +101,10 @@ public void whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisable readOnlyOverride = true; when(formEntryPrompt.isReadOnly()).thenReturn(false); - assertThat(getSpyWidget().captureButton.getVisibility(), is(View.GONE)); - assertThat(getSpyWidget().chooseButton.getVisibility(), is(View.GONE)); - assertThat(getSpyWidget().playButton.getVisibility(), is(View.VISIBLE)); - assertThat(getSpyWidget().playButton.isEnabled(), is(Boolean.FALSE)); - assertThat(getSpyWidget().playButton.getText(), is("Play Video")); + assertThat(getSpyWidget().binding.recordVideoButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.chooseVideoButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.playVideoButton.getVisibility(), is(View.VISIBLE)); + assertThat(getSpyWidget().binding.playVideoButton.isEnabled(), is(Boolean.FALSE)); + assertThat(getSpyWidget().binding.playVideoButton.getText(), is("Play Video")); } } From 91f63a7e9db02b8c226fb7a40a4c29890e3a434e Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Sat, 23 Dec 2023 23:50:20 +0100 Subject: [PATCH 008/750] Reworked the ExPrinterWidget --- .../android/widgets/ExPrinterWidget.java | 134 +++++++++--------- .../src/main/res/layout/ex_printer_widget.xml | 17 +++ .../android/widgets/ExPrinterWidgetTest.java | 2 +- 3 files changed, 87 insertions(+), 66 deletions(-) create mode 100644 collect_app/src/main/res/layout/ex_printer_widget.xml diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/ExPrinterWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/ExPrinterWidget.java index 2fdc75e3ecc..4d777e4724d 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/ExPrinterWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/ExPrinterWidget.java @@ -14,23 +14,24 @@ package org.odk.collect.android.widgets; -import static org.odk.collect.android.formentry.questions.WidgetViewUtils.createSimpleButton; - import android.annotation.SuppressLint; +import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.view.KeyEvent; -import android.widget.Button; -import android.widget.LinearLayout; +import android.view.View; import android.widget.Toast; +import androidx.annotation.NonNull; + import org.javarosa.core.model.data.IAnswerData; import org.odk.collect.analytics.Analytics; import org.odk.collect.android.analytics.AnalyticsEvents; +import org.javarosa.form.api.FormEntryPrompt; +import org.odk.collect.android.databinding.ExPrinterWidgetBinding; import org.odk.collect.android.formentry.questions.QuestionDetails; -import org.odk.collect.android.widgets.interfaces.ButtonClickListener; import org.odk.collect.android.widgets.interfaces.WidgetDataReceiver; import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry; @@ -118,72 +119,31 @@ * @author mitchellsundt@gmail.com */ @SuppressLint("ViewConstructor") -public class ExPrinterWidget extends QuestionWidget implements WidgetDataReceiver, ButtonClickListener { - - final Button launchIntentButton; +public class ExPrinterWidget extends QuestionWidget implements WidgetDataReceiver { + ExPrinterWidgetBinding binding; private final WaitingForDataRegistry waitingForDataRegistry; public ExPrinterWidget(Context context, QuestionDetails prompt, WaitingForDataRegistry waitingForDataRegistry) { super(context, prompt); - render(); - this.waitingForDataRegistry = waitingForDataRegistry; - - String v = getFormEntryPrompt().getSpecialFormQuestionText("buttonText"); - String buttonText = (v != null) ? v : context.getString(org.odk.collect.strings.R.string.launch_printer); - launchIntentButton = createSimpleButton(getContext(), getFormEntryPrompt().isReadOnly(), buttonText, this, false); - - // finish complex layout - LinearLayout printLayout = new LinearLayout(getContext()); - printLayout.setOrientation(LinearLayout.VERTICAL); - printLayout.addView(launchIntentButton); - addAnswerView(printLayout); + render(); } - protected void firePrintingActivity(String intentName) throws ActivityNotFoundException { - - String s = getFormEntryPrompt().getAnswerText(); - - Intent i = new Intent(intentName); - getContext().startActivity(i); - - String[] splits; - if (s != null) { - splits = s.split("
"); - } else { - splits = null; + @Override + protected View onCreateAnswerView(@NonNull Context context, @NonNull FormEntryPrompt prompt, int answerFontSize) { + binding = ExPrinterWidgetBinding.inflate(((Activity) context).getLayoutInflater()); + binding.printButton.setOnClickListener(v -> onButtonClick()); + String customButtonText = getFormEntryPrompt().getSpecialFormQuestionText("buttonText"); + if (customButtonText != null) { + binding.printButton.setText(customButtonText); + binding.printButton.setContentDescription(customButtonText); } - Bundle printDataBundle = new Bundle(); - - String e; - if (splits != null) { - if (splits.length >= 1) { - e = splits[0]; - if (e.length() > 0) { - printDataBundle.putString("BARCODE", e); - } - } - if (splits.length >= 2) { - e = splits[1]; - if (e.length() > 0) { - printDataBundle.putString("QRCODE", e); - } - } - if (splits.length > 2) { - String[] text = new String[splits.length - 2]; - for (int j = 2; j < splits.length; ++j) { - e = splits[j]; - text[j - 2] = e; - } - printDataBundle.putStringArray("TEXT-STRINGS", text); - } + if (questionDetails.isReadOnly()) { + binding.printButton.setVisibility(View.GONE); } - //send the printDataBundle to the activity via broadcast intent - Intent bcastIntent = new Intent(intentName + ".data"); - bcastIntent.putExtra("DATA", printDataBundle); - getContext().sendBroadcast(bcastIntent); + return binding.getRoot(); } @Override @@ -207,17 +167,16 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { @Override public void setOnLongClickListener(OnLongClickListener l) { - launchIntentButton.setOnLongClickListener(l); + binding.printButton.setOnLongClickListener(l); } @Override public void cancelLongPress() { super.cancelLongPress(); - launchIntentButton.cancelLongPress(); + binding.printButton.cancelLongPress(); } - @Override - public void onButtonClick(int buttonId) { + private void onButtonClick() { String appearance = getFormEntryPrompt().getAppearanceHint(); String[] attrs = appearance.split(":"); final String intentName = (attrs.length < 2 || attrs[1].length() == 0) @@ -236,4 +195,49 @@ public void onButtonClick(int buttonId) { .show(); } } -} + + private void firePrintingActivity(String intentName) throws ActivityNotFoundException { + String s = getFormEntryPrompt().getAnswerText(); + + Intent i = new Intent(intentName); + getContext().startActivity(i); + + String[] splits; + if (s != null) { + splits = s.split("
"); + } else { + splits = null; + } + + Bundle printDataBundle = new Bundle(); + + String e; + if (splits != null) { + if (splits.length >= 1) { + e = splits[0]; + if (e.length() > 0) { + printDataBundle.putString("BARCODE", e); + } + } + if (splits.length >= 2) { + e = splits[1]; + if (e.length() > 0) { + printDataBundle.putString("QRCODE", e); + } + } + if (splits.length > 2) { + String[] text = new String[splits.length - 2]; + for (int j = 2; j < splits.length; ++j) { + e = splits[j]; + text[j - 2] = e; + } + printDataBundle.putStringArray("TEXT-STRINGS", text); + } + } + + //send the printDataBundle to the activity via broadcast intent + Intent bcastIntent = new Intent(intentName + ".data"); + bcastIntent.putExtra("DATA", printDataBundle); + getContext().sendBroadcast(bcastIntent); + } +} \ No newline at end of file diff --git a/collect_app/src/main/res/layout/ex_printer_widget.xml b/collect_app/src/main/res/layout/ex_printer_widget.xml new file mode 100644 index 00000000000..c2a7321bf7d --- /dev/null +++ b/collect_app/src/main/res/layout/ex_printer_widget.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/ExPrinterWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/ExPrinterWidgetTest.java index 793dccd9eb0..bd0cbb324ce 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/ExPrinterWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/ExPrinterWidgetTest.java @@ -48,6 +48,6 @@ public void getAnswerShouldReturnExistingAnswerIfPromptHasExistingAnswer() { public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() { when(formEntryPrompt.isReadOnly()).thenReturn(true); - assertThat(getSpyWidget().launchIntentButton.getVisibility(), is(View.GONE)); + assertThat(getSpyWidget().binding.printButton.getVisibility(), is(View.GONE)); } } From 5cb7db477e5e506caa6aac67d95b8e1eade59929 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Sun, 24 Dec 2023 00:07:13 +0100 Subject: [PATCH 009/750] Reworked the LikertWidget --- .../android/widgets/items/LikertWidget.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/items/LikertWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/items/LikertWidget.java index c6ba7d25bdc..8994dcfe7d5 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/items/LikertWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/items/LikertWidget.java @@ -18,6 +18,8 @@ import android.widget.RelativeLayout; import android.widget.TextView; +import androidx.annotation.NonNull; + import com.google.android.material.radiobutton.MaterialRadioButton; import org.javarosa.core.model.SelectChoice; @@ -27,6 +29,7 @@ import org.javarosa.core.reference.InvalidReferenceException; import org.javarosa.core.reference.ReferenceManager; import org.javarosa.form.api.FormEntryCaption; +import org.javarosa.form.api.FormEntryPrompt; import org.odk.collect.android.dynamicpreload.ExternalSelectChoice; import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.utilities.HtmlUtils; @@ -42,7 +45,6 @@ @SuppressLint("ViewConstructor") public class LikertWidget extends QuestionWidget { - LinearLayout view; private RadioButton checkedButton; private final LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, 1); @@ -56,16 +58,18 @@ public class LikertWidget extends QuestionWidget { public LikertWidget(Context context, QuestionDetails questionDetails, SelectChoiceLoader selectChoiceLoader) { super(context, questionDetails); - render(); - items = ItemsWidgetUtils.loadItemsAndHandleErrors(this, questionDetails.getPrompt(), selectChoiceLoader); setMainViewLayoutParameters(); setStructures(); - setButtonListener(); setSavedButton(); - addAnswerView(view); + render(); + } + + @Override + protected View onCreateAnswerView(@NonNull Context context, @NonNull FormEntryPrompt prompt, int answerFontSize) { + return view; } public void setMainViewLayoutParameters() { From 95e3b780c7123c726b18d450d56b15dd940a1af5 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Sun, 24 Dec 2023 00:27:59 +0100 Subject: [PATCH 010/750] Reworked text widgets --- .../org/odk/collect/android/widgets/DecimalWidget.java | 1 - .../org/odk/collect/android/widgets/ExDecimalWidget.java | 1 - .../org/odk/collect/android/widgets/ExIntegerWidget.java | 1 - .../org/odk/collect/android/widgets/ExStringWidget.java | 8 +++++--- .../org/odk/collect/android/widgets/IntegerWidget.java | 1 - .../odk/collect/android/widgets/StringNumberWidget.java | 1 - .../org/odk/collect/android/widgets/StringWidget.java | 7 ++++--- 7 files changed, 9 insertions(+), 11 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/DecimalWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/DecimalWidget.java index 55d2929832a..a92f7963559 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/DecimalWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/DecimalWidget.java @@ -30,7 +30,6 @@ public class DecimalWidget extends StringWidget { public DecimalWidget(Context context, QuestionDetails questionDetails) { super(context, questionDetails); - render(); boolean useThousandSeparator = Appearances.useThousandSeparator(questionDetails.getPrompt()); Double answer = StringWidgetUtils.getDoubleAnswerValueFromIAnswerData(questionDetails.getPrompt().getAnswerValue()); diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/ExDecimalWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/ExDecimalWidget.java index a4faaedc2a4..0014159601f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/ExDecimalWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/ExDecimalWidget.java @@ -41,7 +41,6 @@ public class ExDecimalWidget extends ExStringWidget { public ExDecimalWidget(Context context, QuestionDetails questionDetails, WaitingForDataRegistry waitingForDataRegistry, StringRequester stringRequester) { super(context, questionDetails, waitingForDataRegistry, stringRequester); - render(); boolean useThousandSeparator = Appearances.useThousandSeparator(questionDetails.getPrompt()); Double answer = StringWidgetUtils.getDoubleAnswerValueFromIAnswerData(questionDetails.getPrompt().getAnswerValue()); diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/ExIntegerWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/ExIntegerWidget.java index 6714d74caaf..65861df4968 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/ExIntegerWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/ExIntegerWidget.java @@ -41,7 +41,6 @@ public class ExIntegerWidget extends ExStringWidget { public ExIntegerWidget(Context context, QuestionDetails questionDetails, WaitingForDataRegistry waitingForDataRegistry, StringRequester stringRequester) { super(context, questionDetails, waitingForDataRegistry, stringRequester); - render(); boolean useThousandSeparator = Appearances.useThousandSeparator(questionDetails.getPrompt()); Integer answer = StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(questionDetails.getPrompt().getAnswerValue()); diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/ExStringWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/ExStringWidget.java index 55df95ad7cf..be009559f08 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/ExStringWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/ExStringWidget.java @@ -21,15 +21,18 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; +import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import org.javarosa.core.model.data.StringData; +import org.javarosa.form.api.FormEntryPrompt; import org.odk.collect.android.dynamicpreload.ExternalAppsUtils; import org.odk.collect.android.R; import org.odk.collect.android.formentry.questions.QuestionDetails; @@ -91,7 +94,6 @@ public class ExStringWidget extends StringWidget implements WidgetDataReceiver, public ExStringWidget(Context context, QuestionDetails questionDetails, WaitingForDataRegistry waitingForDataRegistry, StringRequester stringRequester) { super(context, questionDetails); - render(); this.waitingForDataRegistry = waitingForDataRegistry; this.stringRequester = stringRequester; @@ -99,7 +101,7 @@ public ExStringWidget(Context context, QuestionDetails questionDetails, WaitingF } @Override - protected void setUpLayout(Context context) { + protected View onCreateAnswerView(@NonNull Context context, @NonNull FormEntryPrompt prompt, int answerFontSize) { launchIntentButton = createSimpleButton(getContext(), getFormEntryPrompt().isReadOnly(), getButtonText(), this, false); widgetAnswerText.setAnswer(getFormEntryPrompt().getAnswerText()); @@ -115,7 +117,7 @@ protected void setUpLayout(Context context) { answerLayout.setOrientation(LinearLayout.VERTICAL); answerLayout.addView(launchIntentButton); answerLayout.addView(widgetAnswerText); - addAnswerView(answerLayout); + return answerLayout; } private String getButtonText() { diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/IntegerWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/IntegerWidget.java index adc9eaaedc7..23e9a75c8ed 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/IntegerWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/IntegerWidget.java @@ -30,7 +30,6 @@ public class IntegerWidget extends StringWidget { public IntegerWidget(Context context, QuestionDetails questionDetails) { super(context, questionDetails); - render(); boolean useThousandSeparator = Appearances.useThousandSeparator(questionDetails.getPrompt()); Integer answer = StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(questionDetails.getPrompt().getAnswerValue()); diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/StringNumberWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/StringNumberWidget.java index 0a57e121903..f5e3c8b1f1b 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/StringNumberWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/StringNumberWidget.java @@ -30,7 +30,6 @@ public class StringNumberWidget extends StringWidget { public StringNumberWidget(Context context, QuestionDetails questionDetails) { super(context, questionDetails); - render(); boolean useThousandSeparator = Appearances.useThousandSeparator(questionDetails.getPrompt()); String answer = questionDetails.getPrompt().getAnswerValue() == null diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/StringWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/StringWidget.java index 07207da40ad..0d47b8ea5f9 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/StringWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/StringWidget.java @@ -52,12 +52,13 @@ protected StringWidget(Context context, QuestionDetails questionDetails) { this::widgetValueChanged ); - setUpLayout(context); + render(); } - protected void setUpLayout(Context context) { + @Override + protected View onCreateAnswerView(@NonNull Context context, @NonNull FormEntryPrompt prompt, int answerFontSize) { setDisplayValueFromModel(); - addAnswerView(widgetAnswerText); + return widgetAnswerText; } @Override From c92ae2952cb13e6e1bf0d1a68bb3e95a6ce1c9db Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Sun, 24 Dec 2023 00:32:03 +0100 Subject: [PATCH 011/750] Removed the deprecated method --- .../android/widgets/QuestionWidget.java | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/QuestionWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/QuestionWidget.java index 80630660e2b..23a2e319960 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/QuestionWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/QuestionWidget.java @@ -145,7 +145,15 @@ public void render() { ); if (answerView != null) { - addAnswerView(answerView); + ViewGroup answerContainer = findViewById(R.id.answer_container); + + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE); + + answerContainer.addView(answerView, params); + + adjustButtonFontSize(answerContainer); } } @@ -305,22 +313,6 @@ private TextView setupHelpText(TextView helpText, FormEntryPrompt prompt) { } } - /** - * Widget should use {@link #onCreateAnswerView} to define answer view - */ - @Deprecated - protected final void addAnswerView(View v) { - ViewGroup answerContainer = findViewById(R.id.answer_container); - - RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE); - - answerContainer.addView(v, params); - - adjustButtonFontSize(answerContainer); - } - private void hideAnswerContainerIfNeeded() { if (questionDetails.isReadOnly() && formEntryPrompt.getAnswerValue() == null) { findViewById(R.id.answer_container).setVisibility(GONE); From 94840e14395e361c2562338519f5762dbaba555f Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 29 Dec 2023 14:39:53 +0100 Subject: [PATCH 012/750] Removed redundant file --- .../src/main/res/layout/named_view_ids.xml | 54 ------------------- 1 file changed, 54 deletions(-) delete mode 100644 collect_app/src/main/res/layout/named_view_ids.xml diff --git a/collect_app/src/main/res/layout/named_view_ids.xml b/collect_app/src/main/res/layout/named_view_ids.xml deleted file mode 100644 index 0f8c8d53371..00000000000 --- a/collect_app/src/main/res/layout/named_view_ids.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 1da8db8c10412cb2fe56995d26465cda2b0ebc75 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 29 Dec 2023 15:08:03 +0100 Subject: [PATCH 013/750] Fixed IntentGroupTest --- .../feature/formentry/IntentGroupTest.java | 8 +-- .../collect/android/widgets/AudioWidget.java | 34 +++++------ .../main/res/layout/audio_widget_answer.xml | 4 +- .../android/widgets/AudioWidgetTest.java | 58 +++++++++---------- 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/IntentGroupTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/IntentGroupTest.java index d47e586c3eb..82184b09682 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/IntentGroupTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/IntentGroupTest.java @@ -208,8 +208,8 @@ public void collect_shouldNotCrashWhenAnyErrorIsThrownWhileReceivingAnswer() { private void assertImageWidgetWithoutAnswer() { onView(allOf(withTagValue(is("ImageView")), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))).check(doesNotExist()); - onView(withId(R.id.capture_image)).check(doesNotExist()); - onView(withId(R.id.choose_image)).check(doesNotExist()); + onView(withId(R.id.capture_button)).check(matches(not(isDisplayed()))); + onView(withId(R.id.choose_button)).check(matches(not(isDisplayed()))); } private void assertAudioWidgetWithoutAnswer() { @@ -227,8 +227,8 @@ private void assertFileWidgetWithoutAnswer() { private void assertImageWidgetWithAnswer() { onView(allOf(withTagValue(is("ImageView")), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))).check(matches(not(doesNotExist()))); - onView(withId(R.id.capture_image)).check(doesNotExist()); - onView(withId(R.id.choose_image)).check(doesNotExist()); + onView(withId(R.id.capture_button)).check(matches(not(isDisplayed()))); + onView(withId(R.id.choose_button)).check(matches(not(isDisplayed()))); } private void assertAudioWidgetWithAnswer() { diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/AudioWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/AudioWidget.java index 0d7fba4cf12..381c3507f8f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/AudioWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/AudioWidget.java @@ -81,8 +81,8 @@ public AudioWidget(Context context, QuestionDetails questionDetails, QuestionMed updatePlayerMedia(); recordingStatusHandler.onBlockedStatusChange(isRecordingBlocked -> { - binding.captureButton.setEnabled(!isRecordingBlocked); - binding.chooseButton.setEnabled(!isRecordingBlocked); + binding.recordAudioButton.setEnabled(!isRecordingBlocked); + binding.chooseAudioButton.setEnabled(!isRecordingBlocked); }); recordingStatusHandler.onRecordingStatusChange(getFormEntryPrompt(), session -> { @@ -104,12 +104,12 @@ public AudioWidget(Context context, QuestionDetails questionDetails, QuestionMed protected View onCreateAnswerView(Context context, FormEntryPrompt prompt, int answerFontSize) { binding = AudioWidgetAnswerBinding.inflate(LayoutInflater.from(context)); - binding.captureButton.setOnClickListener(v -> { + binding.recordAudioButton.setOnClickListener(v -> { hideError(); binding.audioPlayer.waveform.clear(); recordingRequester.requestRecording(getFormEntryPrompt()); }); - binding.chooseButton.setOnClickListener(v -> audioFileRequester.requestFile(getFormEntryPrompt())); + binding.chooseAudioButton.setOnClickListener(v -> audioFileRequester.requestFile(getFormEntryPrompt())); return binding.getRoot(); } @@ -161,32 +161,32 @@ public void setData(Object object) { private void updateVisibilities() { if (recordingInProgress) { - binding.captureButton.setVisibility(GONE); - binding.chooseButton.setVisibility(GONE); + binding.recordAudioButton.setVisibility(GONE); + binding.chooseAudioButton.setVisibility(GONE); binding.audioPlayer.recordingDuration.setVisibility(VISIBLE); binding.audioPlayer.waveform.setVisibility(VISIBLE); binding.audioPlayer.audioController.setVisibility(GONE); } else if (getAnswer() == null) { - binding.captureButton.setVisibility(VISIBLE); - binding.chooseButton.setVisibility(VISIBLE); + binding.recordAudioButton.setVisibility(VISIBLE); + binding.chooseAudioButton.setVisibility(VISIBLE); binding.audioPlayer.recordingDuration.setVisibility(GONE); binding.audioPlayer.waveform.setVisibility(GONE); binding.audioPlayer.audioController.setVisibility(GONE); } else { - binding.captureButton.setVisibility(GONE); - binding.chooseButton.setVisibility(GONE); + binding.recordAudioButton.setVisibility(GONE); + binding.chooseAudioButton.setVisibility(GONE); binding.audioPlayer.recordingDuration.setVisibility(GONE); binding.audioPlayer.waveform.setVisibility(GONE); binding.audioPlayer.audioController.setVisibility(VISIBLE); } if (questionDetails.isReadOnly()) { - binding.captureButton.setVisibility(GONE); - binding.chooseButton.setVisibility(GONE); + binding.recordAudioButton.setVisibility(GONE); + binding.chooseAudioButton.setVisibility(GONE); } if (getFormEntryPrompt().getAppearanceHint() != null && getFormEntryPrompt().getAppearanceHint().toLowerCase(Locale.ENGLISH).contains(Appearances.NEW)) { - binding.chooseButton.setVisibility(GONE); + binding.chooseAudioButton.setVisibility(GONE); } } @@ -236,15 +236,15 @@ private Integer getDurationOfFile(String uri) { @Override public void setOnLongClickListener(OnLongClickListener l) { - binding.captureButton.setOnLongClickListener(l); - binding.chooseButton.setOnLongClickListener(l); + binding.recordAudioButton.setOnLongClickListener(l); + binding.chooseAudioButton.setOnLongClickListener(l); } @Override public void cancelLongPress() { super.cancelLongPress(); - binding.captureButton.cancelLongPress(); - binding.chooseButton.cancelLongPress(); + binding.recordAudioButton.cancelLongPress(); + binding.chooseAudioButton.cancelLongPress(); } /** diff --git a/collect_app/src/main/res/layout/audio_widget_answer.xml b/collect_app/src/main/res/layout/audio_widget_answer.xml index 19485dd941d..1891f18e726 100644 --- a/collect_app/src/main/res/layout/audio_widget_answer.xml +++ b/collect_app/src/main/res/layout/audio_widget_answer.xml @@ -5,14 +5,14 @@ android:orientation="vertical"> Date: Thu, 29 Feb 2024 16:27:31 +0100 Subject: [PATCH 014/750] Fixed module names in test_modules.txt --- .circleci/test_modules.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/test_modules.txt b/.circleci/test_modules.txt index e1d92308272..9aaf86bdc08 100644 --- a/.circleci/test_modules.txt +++ b/.circleci/test_modules.txt @@ -1,10 +1,10 @@ shared -formstest +forms-test androidshared async strings -audioclips -audiorecorder +audio-clips +audio-recorder projects location geo From 9fbe0b0486cee4d777eb107b5c2b92be7e0355a9 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 29 Feb 2024 14:38:49 +0100 Subject: [PATCH 015/750] Reworked the ex string widgets to use their own xml layout --- .../formentry/questions/WidgetViewUtils.kt | 42 ------- .../config/AppDependencyComponent.java | 3 - .../android/widgets/ExDecimalWidget.java | 6 +- .../android/widgets/ExIntegerWidget.java | 6 +- .../android/widgets/ExStringWidget.java | 116 +++++++++++------- .../collect/android/widgets/StringWidget.java | 34 +---- .../interfaces/ButtonClickListener.java | 21 ---- .../widgets/utilities/StringWidgetUtils.java | 29 +++++ .../res/layout/ex_string_question_type.xml | 22 ++++ .../main/res/layout/widget_answer_button.xml | 7 -- collect_app/src/main/res/values/buttons.xml | 2 +- .../android/widgets/AudioWidgetTest.java | 2 +- .../android/widgets/ExDecimalWidgetTest.java | 12 +- .../android/widgets/ExIntegerWidgetTest.java | 12 +- .../base/GeneralExStringWidgetTest.java | 13 +- 15 files changed, 151 insertions(+), 176 deletions(-) delete mode 100644 collect_app/src/main/java/org/odk/collect/android/formentry/questions/WidgetViewUtils.kt delete mode 100644 collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/ButtonClickListener.java create mode 100644 collect_app/src/main/res/layout/ex_string_question_type.xml delete mode 100644 collect_app/src/main/res/layout/widget_answer_button.xml diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/questions/WidgetViewUtils.kt b/collect_app/src/main/java/org/odk/collect/android/formentry/questions/WidgetViewUtils.kt deleted file mode 100644 index 1eed9b8ef07..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/questions/WidgetViewUtils.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.odk.collect.android.formentry.questions - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.widget.Button -import android.widget.TableLayout -import androidx.annotation.IdRes -import com.google.android.material.button.MaterialButton -import org.odk.collect.android.R -import org.odk.collect.android.widgets.QuestionWidget -import org.odk.collect.android.widgets.interfaces.ButtonClickListener -import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard.allowClick - -object WidgetViewUtils { - @JvmStatic - @JvmOverloads - fun createSimpleButton(context: Context, readOnly: Boolean, text: String?, listener: ButtonClickListener, addMargin: Boolean, @IdRes withId: Int = R.id.simple_button): Button { - val button = LayoutInflater - .from(context) - .inflate(R.layout.widget_answer_button, null, false) as MaterialButton - if (readOnly) { - button.visibility = View.GONE - } else { - button.id = withId - button.text = text - button.contentDescription = text - if (addMargin) { - val params = TableLayout.LayoutParams() - val marginStandard = context.resources.getDimension(org.odk.collect.androidshared.R.dimen.margin_standard).toInt() - params.setMargins(0, marginStandard, 0, 0) - button.layoutParams = params - } - button.setOnClickListener { v: View? -> - if (allowClick(QuestionWidget::class.java.name)) { - listener.onButtonClick(withId) - } - } - } - return button - } -} 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 cbdb89c1696..d402bc1ef7d 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 @@ -80,7 +80,6 @@ import org.odk.collect.android.utilities.InstancesRepositoryProvider; import org.odk.collect.android.utilities.ProjectResetter; import org.odk.collect.android.utilities.ThemeUtils; -import org.odk.collect.android.widgets.ExStringWidget; import org.odk.collect.android.widgets.QuestionWidget; import org.odk.collect.android.widgets.items.SelectOneFromMapDialogFragment; import org.odk.collect.androidshared.network.NetworkStateProvider; @@ -162,8 +161,6 @@ interface Builder { void inject(QuestionWidget questionWidget); - void inject(ExStringWidget exStringWidget); - void inject(ODKView odkView); void inject(FormMetadataPreferencesFragment formMetadataPreferencesFragment); diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/ExDecimalWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/ExDecimalWidget.java index 0014159601f..0dc3d1dc1dc 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/ExDecimalWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/ExDecimalWidget.java @@ -44,7 +44,7 @@ public ExDecimalWidget(Context context, QuestionDetails questionDetails, Waiting boolean useThousandSeparator = Appearances.useThousandSeparator(questionDetails.getPrompt()); Double answer = StringWidgetUtils.getDoubleAnswerValueFromIAnswerData(questionDetails.getPrompt().getAnswerValue()); - widgetAnswerText.setDecimalType(useThousandSeparator, answer); + binding.widgetAnswerText.setDecimalType(useThousandSeparator, answer); } @Override @@ -59,12 +59,12 @@ protected int getRequestCode() { @Override public IAnswerData getAnswer() { - return StringWidgetUtils.getDecimalData(widgetAnswerText.getAnswer(), getFormEntryPrompt()); + return StringWidgetUtils.getDecimalData(binding.widgetAnswerText.getAnswer(), getFormEntryPrompt()); } @Override public void setData(Object answer) { DecimalData decimalData = ExternalAppsUtils.asDecimalData(answer); - widgetAnswerText.setAnswer(decimalData == null ? null : decimalData.getValue().toString()); + binding.widgetAnswerText.setAnswer(decimalData == null ? null : decimalData.getValue().toString()); } } diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/ExIntegerWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/ExIntegerWidget.java index 65861df4968..33a64406217 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/ExIntegerWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/ExIntegerWidget.java @@ -44,7 +44,7 @@ public ExIntegerWidget(Context context, QuestionDetails questionDetails, Waiting boolean useThousandSeparator = Appearances.useThousandSeparator(questionDetails.getPrompt()); Integer answer = StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(questionDetails.getPrompt().getAnswerValue()); - widgetAnswerText.setIntegerType(useThousandSeparator, answer); + binding.widgetAnswerText.setIntegerType(useThousandSeparator, answer); } @Override @@ -59,12 +59,12 @@ protected int getRequestCode() { @Override public IAnswerData getAnswer() { - return StringWidgetUtils.getIntegerData(widgetAnswerText.getAnswer(), getFormEntryPrompt()); + return StringWidgetUtils.getIntegerData(binding.widgetAnswerText.getAnswer(), getFormEntryPrompt()); } @Override public void setData(Object answer) { IntegerData integerData = ExternalAppsUtils.asIntegerData(answer); - widgetAnswerText.setAnswer(integerData == null ? null : integerData.getValue().toString()); + binding.widgetAnswerText.setAnswer(integerData == null ? null : integerData.getValue().toString()); } } diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/ExStringWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/ExStringWidget.java index be009559f08..a0741c9e114 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/ExStringWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/ExStringWidget.java @@ -14,31 +14,32 @@ package org.odk.collect.android.widgets; -import static org.odk.collect.android.formentry.questions.WidgetViewUtils.createSimpleButton; -import static org.odk.collect.android.injection.DaggerUtils.getComponent; import static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; -import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; +import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.data.StringData; import org.javarosa.form.api.FormEntryPrompt; +import org.odk.collect.android.databinding.ExStringQuestionTypeBinding; import org.odk.collect.android.dynamicpreload.ExternalAppsUtils; import org.odk.collect.android.R; import org.odk.collect.android.formentry.questions.QuestionDetails; -import org.odk.collect.android.widgets.interfaces.ButtonClickListener; import org.odk.collect.android.widgets.interfaces.WidgetDataReceiver; +import org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils; import org.odk.collect.android.widgets.utilities.StringRequester; +import org.odk.collect.android.widgets.utilities.StringWidgetUtils; import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry; import java.io.Serializable; @@ -85,39 +86,44 @@ * */ @SuppressLint("ViewConstructor") -public class ExStringWidget extends StringWidget implements WidgetDataReceiver, ButtonClickListener { +public class ExStringWidget extends QuestionWidget implements WidgetDataReceiver { + public ExStringQuestionTypeBinding binding; private final WaitingForDataRegistry waitingForDataRegistry; private boolean hasExApp = true; - public Button launchIntentButton; private final StringRequester stringRequester; public ExStringWidget(Context context, QuestionDetails questionDetails, WaitingForDataRegistry waitingForDataRegistry, StringRequester stringRequester) { super(context, questionDetails); + render(); this.waitingForDataRegistry = waitingForDataRegistry; this.stringRequester = stringRequester; - getComponent(context).inject(this); } @Override protected View onCreateAnswerView(@NonNull Context context, @NonNull FormEntryPrompt prompt, int answerFontSize) { - launchIntentButton = createSimpleButton(getContext(), getFormEntryPrompt().isReadOnly(), getButtonText(), this, false); - - widgetAnswerText.setAnswer(getFormEntryPrompt().getAnswerText()); - ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT + binding = ExStringQuestionTypeBinding.inflate(LayoutInflater.from(context)); + binding.launchAppButton.setText(getButtonText()); + binding.launchAppButton.setOnClickListener(v -> { + waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex()); + stringRequester.launch((Activity) getContext(), getRequestCode(), getFormEntryPrompt(), getAnswerForIntent(), (String errorMsg) -> { + onException(errorMsg); + return null; + }); + }); + if (questionDetails.isReadOnly()) { + binding.launchAppButton.setVisibility(GONE); + } + binding.widgetAnswerText.init( + QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.HEADLINE_6), + true, + StringWidgetUtils.getNumberOfRows(questionDetails.getPrompt()), + this::widgetValueChanged ); - int marginTop = (int) getContext().getResources().getDimension(org.odk.collect.androidshared.R.dimen.margin_standard); - params.setMargins(0, marginTop, 0, 0); - widgetAnswerText.setLayoutParams(params); + binding.widgetAnswerText.setAnswer(getFormEntryPrompt().getAnswerText()); - LinearLayout answerLayout = new LinearLayout(getContext()); - answerLayout.setOrientation(LinearLayout.VERTICAL); - answerLayout.addView(launchIntentButton); - answerLayout.addView(widgetAnswerText); - return answerLayout; + return binding.getRoot(); } private String getButtonText() { @@ -133,21 +139,33 @@ protected int getRequestCode() { return RequestCodes.EX_STRING_CAPTURE; } + @Nullable + @Override + public IAnswerData getAnswer() { + String answer = binding.widgetAnswerText.getAnswer(); + return !answer.isEmpty() ? new StringData(answer) : null; + } + + @Override + public void clearAnswer() { + binding.widgetAnswerText.clearAnswer(); + } + @Override public void setData(Object answer) { StringData stringData = ExternalAppsUtils.asStringData(answer); - widgetAnswerText.setAnswer(stringData == null ? null : stringData.getValue().toString()); + binding.widgetAnswerText.setAnswer(stringData == null ? null : stringData.getValue().toString()); } @Override public void setFocus(Context context) { if (hasExApp) { - widgetAnswerText.setFocus(false); + binding.widgetAnswerText.setFocus(false); // focus on launch button - launchIntentButton.requestFocus(); + binding.launchAppButton.requestFocus(); } else { if (!getFormEntryPrompt().isReadOnly()) { - widgetAnswerText.setFocus(true); + binding.widgetAnswerText.setFocus(true); /* * If you do a multi-question screen after a "add another group" dialog, this won't * automatically pop up. It's an Android issue. @@ -159,44 +177,52 @@ public void setFocus(Context context) { * is focused before the dialog pops up, everything works fine. great. */ } else { - widgetAnswerText.setFocus(false); + binding.widgetAnswerText.setFocus(false); } } } @Override public void setOnLongClickListener(OnLongClickListener l) { - widgetAnswerText.setOnLongClickListener(l); - launchIntentButton.setOnLongClickListener(l); + binding.widgetAnswerText.setOnLongClickListener(l); + binding.launchAppButton.setOnLongClickListener(l); } @Override public void cancelLongPress() { super.cancelLongPress(); - widgetAnswerText.cancelLongPress(); - launchIntentButton.cancelLongPress(); + binding.widgetAnswerText.cancelLongPress(); + binding.launchAppButton.cancelLongPress(); } + /** + * Registers all subviews except for the answer_container (which contains the EditText) to clear on long press. + * This makes it possible to long-press to paste or perform other text editing functions. + */ @Override - public void onButtonClick(int buttonId) { - waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex()); - stringRequester.launch((Activity) getContext(), getRequestCode(), getFormEntryPrompt(), getAnswerForIntent(), (String errorMsg) -> { - onException(errorMsg); - return null; - }); + protected void registerToClearAnswerOnLongPress(Activity activity, ViewGroup viewGroup) { + ViewGroup view = findViewById(R.id.question_widget_container); + for (int i = 0; i < view.getChildCount(); i++) { + View childView = view.getChildAt(i); + if (childView.getId() != R.id.answer_container) { + childView.setTag(childView.getId()); + childView.setId(getId()); + activity.registerForContextMenu(childView); + } + } } private void focusAnswer() { - widgetAnswerText.setFocus(true); + binding.widgetAnswerText.setFocus(true); } private void onException(String toastText) { hasExApp = false; if (!getFormEntryPrompt().isReadOnly()) { - widgetAnswerText.updateState(false); + binding.widgetAnswerText.updateState(false); } - launchIntentButton.setEnabled(false); - launchIntentButton.setFocusable(false); + binding.launchAppButton.setEnabled(false); + binding.launchAppButton.setFocusable(false); waitingForDataRegistry.cancelWaitingForData(); Toast.makeText(getContext(), @@ -208,16 +234,16 @@ private void onException(String toastText) { @Override public void hideError() { - super.hideError(); - errorLayout.setVisibility(GONE); + binding.widgetAnswerText.setError(null); } @Override public void displayError(String errorMessage) { hideError(); - if (widgetAnswerText.isEditableState()) { - super.displayError(errorMessage); + if (binding.widgetAnswerText.isEditableState()) { + binding.widgetAnswerText.setError(errorMessage); + setBackground(ContextCompat.getDrawable(getContext(), R.drawable.question_with_error_border)); } else { ((TextView) errorLayout.findViewById(R.id.error_message)).setText(errorMessage); errorLayout.setVisibility(VISIBLE); diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/StringWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/StringWidget.java index 0d47b8ea5f9..1bc55fab2b5 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/StringWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/StringWidget.java @@ -23,7 +23,6 @@ import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; -import org.javarosa.core.model.QuestionDef; import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.data.StringData; import org.javarosa.form.api.FormEntryPrompt; @@ -31,8 +30,7 @@ import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.views.WidgetAnswerText; import org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils; - -import timber.log.Timber; +import org.odk.collect.android.widgets.utilities.StringWidgetUtils; /** * The most basic widget that allows for entry of any text. @@ -47,8 +45,8 @@ protected StringWidget(Context context, QuestionDetails questionDetails) { widgetAnswerText = new WidgetAnswerText(context); widgetAnswerText.init( QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.HEADLINE_6), - questionDetails.isReadOnly() || this instanceof ExStringWidget, - getNumberOfRows(questionDetails.getPrompt()), + questionDetails.isReadOnly(), + StringWidgetUtils.getNumberOfRows(questionDetails.getPrompt()), this::widgetValueChanged ); @@ -118,32 +116,6 @@ public void setDisplayValueFromModel() { } } - private Integer getNumberOfRows(FormEntryPrompt prompt) { - QuestionDef questionDef = prompt.getQuestion(); - if (questionDef != null) { - /* - * If a 'rows' attribute is on the input tag, set the minimum number of lines - * to display in the field to that value. - * - * I.e., - * - * ... - * - * - * will set the height of the EditText box to 5 rows high. - */ - String height = questionDef.getAdditionalAttribute(null, "rows"); - if (height != null && height.length() != 0) { - try { - return Integer.parseInt(height); - } catch (Exception e) { - Timber.e(new Error("Unable to process the rows setting for the answerText field: " + e)); - } - } - } - return null; - } - @Override public void hideError() { widgetAnswerText.setError(null); diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/ButtonClickListener.java b/collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/ButtonClickListener.java deleted file mode 100644 index f884120c8e0..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/ButtonClickListener.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2017 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.widgets.interfaces; - -public interface ButtonClickListener { - void onButtonClick(int buttonId); -} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/StringWidgetUtils.java b/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/StringWidgetUtils.java index 6331a54cd0c..33bcaae6f8b 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/StringWidgetUtils.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/StringWidgetUtils.java @@ -1,5 +1,6 @@ package org.odk.collect.android.widgets.utilities; +import org.javarosa.core.model.QuestionDef; import org.javarosa.core.model.data.DecimalData; import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.data.IntegerData; @@ -8,6 +9,8 @@ import org.odk.collect.android.listeners.ThousandsSeparatorTextWatcher; import org.odk.collect.android.utilities.Appearances; +import timber.log.Timber; + public final class StringWidgetUtils { private StringWidgetUtils() { @@ -97,4 +100,30 @@ public static StringData getStringNumberData(String answer, FormEntryPrompt prom } } } + + public static Integer getNumberOfRows(FormEntryPrompt prompt) { + QuestionDef questionDef = prompt.getQuestion(); + if (questionDef != null) { + /* + * If a 'rows' attribute is on the input tag, set the minimum number of lines + * to display in the field to that value. + * + * I.e., + * + * ... + * + * + * will set the height of the EditText box to 5 rows high. + */ + String height = questionDef.getAdditionalAttribute(null, "rows"); + if (height != null && height.length() != 0) { + try { + return Integer.parseInt(height); + } catch (Exception e) { + Timber.e(new Error("Unable to process the rows setting for the answerText field: " + e)); + } + } + } + return null; + } } diff --git a/collect_app/src/main/res/layout/ex_string_question_type.xml b/collect_app/src/main/res/layout/ex_string_question_type.xml new file mode 100644 index 00000000000..8fa835eb6da --- /dev/null +++ b/collect_app/src/main/res/layout/ex_string_question_type.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/collect_app/src/main/res/layout/widget_answer_button.xml b/collect_app/src/main/res/layout/widget_answer_button.xml deleted file mode 100644 index ae0d6307a41..00000000000 --- a/collect_app/src/main/res/layout/widget_answer_button.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/collect_app/src/main/res/values/buttons.xml b/collect_app/src/main/res/values/buttons.xml index 36c44c29f19..c5aa6151354 100644 --- a/collect_app/src/main/res/values/buttons.xml +++ b/collect_app/src/main/res/values/buttons.xml @@ -3,7 +3,7 @@ + diff --git a/collect_app/src/main/res/layout/form_download_list.xml b/collect_app/src/main/res/layout/form_download_list.xml index 42b6a143c73..adf6361b0f8 100644 --- a/collect_app/src/main/res/layout/form_download_list.xml +++ b/collect_app/src/main/res/layout/form_download_list.xml @@ -56,7 +56,7 @@ the License. ?attr/colorSecondary - - From 2d3a939dc070c0f96c1d3894fba71b694b6e6abd Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 1 Mar 2024 13:02:29 +0000 Subject: [PATCH 098/750] Pull out helper for adding vertical divider to lists --- .../androidshared/ui/RecyclerViewUtils.kt | 17 +++++++++++++++ .../main/res/drawable/list_item_divider.xml | 0 .../android/activities/AppListActivity.java | 2 +- .../android/formlists/FormListRecyclerView.kt | 21 ------------------- .../blankformlist/BlankFormListActivity.kt | 7 ++++++- .../blankformlist/DeleteBlankFormFragment.kt | 12 ++--------- .../savedformlist/DeleteSavedFormFragment.kt | 12 ++--------- .../send/InstanceUploaderListActivity.java | 2 +- .../res/layout/activity_blank_form_list.xml | 2 +- 9 files changed, 30 insertions(+), 45 deletions(-) create mode 100644 androidshared/src/main/java/org/odk/collect/androidshared/ui/RecyclerViewUtils.kt rename {collect_app => androidshared}/src/main/res/drawable/list_item_divider.xml (100%) delete mode 100644 collect_app/src/main/java/org/odk/collect/android/formlists/FormListRecyclerView.kt diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/ui/RecyclerViewUtils.kt b/androidshared/src/main/java/org/odk/collect/androidshared/ui/RecyclerViewUtils.kt new file mode 100644 index 00000000000..b2dea3e66a5 --- /dev/null +++ b/androidshared/src/main/java/org/odk/collect/androidshared/ui/RecyclerViewUtils.kt @@ -0,0 +1,17 @@ +package org.odk.collect.androidshared.ui + +import android.content.Context +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.DividerItemDecoration +import org.odk.collect.androidshared.R + +object RecyclerViewUtils { + + fun verticalLineDivider(context: Context): DividerItemDecoration { + val itemDecoration = DividerItemDecoration(context, DividerItemDecoration.VERTICAL) + val drawable = ContextCompat.getDrawable(context, R.drawable.list_item_divider)!! + itemDecoration.setDrawable(drawable) + + return itemDecoration + } +} diff --git a/collect_app/src/main/res/drawable/list_item_divider.xml b/androidshared/src/main/res/drawable/list_item_divider.xml similarity index 100% rename from collect_app/src/main/res/drawable/list_item_divider.xml rename to androidshared/src/main/res/drawable/list_item_divider.xml diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/AppListActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/AppListActivity.java index 9ad0ad9160d..c976d638ee1 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/AppListActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/AppListActivity.java @@ -140,7 +140,7 @@ private void init() { llParent = findViewById(R.id.llParent); // Use the nicer-looking drawable with Material Design insets. - listView.setDivider(ContextCompat.getDrawable(this, R.drawable.list_item_divider)); + listView.setDivider(ContextCompat.getDrawable(this, org.odk.collect.androidshared.R.drawable.list_item_divider)); listView.setDividerHeight(1); setSupportActionBar(findViewById(R.id.toolbar)); diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/FormListRecyclerView.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/FormListRecyclerView.kt deleted file mode 100644 index f1217951166..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/FormListRecyclerView.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.odk.collect.android.formlists - -import android.content.Context -import android.util.AttributeSet -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import org.odk.collect.android.R - -class FormListRecyclerView(context: Context, attrs: AttributeSet?) : RecyclerView(context, attrs) { - - constructor(context: Context) : this(context, null) - - init { - layoutManager = LinearLayoutManager(context) - val itemDecoration = DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL) - itemDecoration.setDrawable(ContextCompat.getDrawable(getContext(), R.drawable.list_item_divider)!!) - addItemDecoration(itemDecoration) - } -} 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 b1d03f45296..61184e474a0 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 @@ -7,6 +7,7 @@ import android.view.View import android.widget.ProgressBar import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import org.odk.collect.android.R import org.odk.collect.android.activities.FormMapActivity @@ -16,6 +17,7 @@ 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.EmptyListView +import org.odk.collect.androidshared.ui.RecyclerViewUtils import org.odk.collect.androidshared.ui.SnackbarUtils import org.odk.collect.permissions.PermissionListener import org.odk.collect.permissions.PermissionsProvider @@ -53,7 +55,10 @@ class BlankFormListActivity : LocalizedActivity(), OnFormItemClickListener { val menuProvider = BlankFormListMenuProvider(this, viewModel, networkStateProvider) addMenuProvider(menuProvider, this) - findViewById(R.id.form_list).adapter = adapter + val list = findViewById(R.id.form_list) + list.layoutManager = LinearLayoutManager(this) + list.addItemDecoration(RecyclerViewUtils.verticalLineDivider(this)) + list.adapter = adapter initObservers() } diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt index 0407a4d02a0..c9623784d19 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt @@ -6,17 +6,16 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.CheckBox -import androidx.core.content.ContextCompat import androidx.core.view.MenuHost import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle.State import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.map -import androidx.recyclerview.widget.DividerItemDecoration import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.odk.collect.android.R import org.odk.collect.androidshared.ui.FragmentFactoryBuilder +import org.odk.collect.androidshared.ui.RecyclerViewUtils import org.odk.collect.androidshared.ui.multiselect.MultiSelectAdapter import org.odk.collect.androidshared.ui.multiselect.MultiSelectControlsFragment import org.odk.collect.androidshared.ui.multiselect.MultiSelectItem @@ -48,14 +47,7 @@ class DeleteBlankFormFragment( it.empty.setTitle(getString(string.empty_list_of_forms_to_delete_title)) it.empty.setSubtitle(getString(string.empty_list_of_blank_forms_to_delete_subtitle)) - it.list.also { - val itemDecoration = - DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL) - val divider = - ContextCompat.getDrawable(requireContext(), R.drawable.list_item_divider)!! - itemDecoration.setDrawable(divider) - it.addItemDecoration(itemDecoration) - } + it.list.addItemDecoration(RecyclerViewUtils.verticalLineDivider(context)) } } .build() diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt index da19f6f6d1e..2e7c29cde8c 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt @@ -5,19 +5,18 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.content.ContextCompat import androidx.core.view.MenuHost import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.map -import androidx.recyclerview.widget.DividerItemDecoration import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.odk.collect.analytics.Analytics import org.odk.collect.android.R import org.odk.collect.android.analytics.AnalyticsEvents import org.odk.collect.androidshared.ui.FragmentFactoryBuilder +import org.odk.collect.androidshared.ui.RecyclerViewUtils import org.odk.collect.androidshared.ui.SnackbarUtils import org.odk.collect.androidshared.ui.SnackbarUtils.SnackbarPresenterObserver import org.odk.collect.androidshared.ui.multiselect.MultiSelectControlsFragment @@ -52,14 +51,7 @@ class DeleteSavedFormFragment( it.empty.setTitle(getString(string.empty_list_of_forms_to_delete_title)) it.empty.setSubtitle(getString(string.empty_list_of_saved_forms_to_delete_subtitle)) - it.list.also { - val itemDecoration = - DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL) - val divider = - ContextCompat.getDrawable(requireContext(), R.drawable.list_item_divider)!! - itemDecoration.setDrawable(divider) - it.addItemDecoration(itemDecoration) - } + it.list.addItemDecoration(RecyclerViewUtils.verticalLineDivider(context)) } } .build() 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 602d05ab8aa..1bca0f33d33 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 @@ -201,7 +201,7 @@ public void setContentView(View view) { progressBar = findViewById(R.id.progressBar); // Use the nicer-looking drawable with Material Design insets. - listView.setDivider(ContextCompat.getDrawable(this, R.drawable.list_item_divider)); + listView.setDivider(ContextCompat.getDrawable(this, org.odk.collect.androidshared.R.drawable.list_item_divider)); listView.setDividerHeight(1); setSupportActionBar(findViewById(R.id.toolbar)); diff --git a/collect_app/src/main/res/layout/activity_blank_form_list.xml b/collect_app/src/main/res/layout/activity_blank_form_list.xml index 79d3d10a6e7..c909118c326 100644 --- a/collect_app/src/main/res/layout/activity_blank_form_list.xml +++ b/collect_app/src/main/res/layout/activity_blank_form_list.xml @@ -7,7 +7,7 @@ - From 808aa246d2e9fc3ce9d901258c040099fec50de8 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 15 Mar 2024 18:54:35 +0100 Subject: [PATCH 099/750] Updated the style used in question buttons to display icons --- collect_app/src/main/res/layout/annotate_widget.xml | 6 +++--- .../src/main/res/layout/arbitrary_file_widget_answer.xml | 2 +- collect_app/src/main/res/layout/audio_widget_answer.xml | 4 ++-- collect_app/src/main/res/layout/barcode_widget_answer.xml | 2 +- collect_app/src/main/res/layout/bearing_widget_answer.xml | 2 +- collect_app/src/main/res/layout/date_widget_answer.xml | 2 +- collect_app/src/main/res/layout/draw_widget.xml | 2 +- .../src/main/res/layout/ex_arbitrary_file_widget_answer.xml | 2 +- collect_app/src/main/res/layout/ex_audio_widget_answer.xml | 2 +- collect_app/src/main/res/layout/ex_image_widget_answer.xml | 2 +- collect_app/src/main/res/layout/ex_printer_widget.xml | 2 +- collect_app/src/main/res/layout/ex_string_question_type.xml | 2 +- collect_app/src/main/res/layout/ex_video_widget_answer.xml | 4 ++-- collect_app/src/main/res/layout/geo_widget_answer.xml | 2 +- collect_app/src/main/res/layout/image_widget.xml | 4 ++-- collect_app/src/main/res/layout/osm_widget_answer.xml | 2 +- collect_app/src/main/res/layout/printer_widget.xml | 2 +- .../src/main/res/layout/range_picker_widget_answer.xml | 2 +- collect_app/src/main/res/layout/ranking_widget.xml | 2 +- .../main/res/layout/select_one_from_map_widget_answer.xml | 2 +- collect_app/src/main/res/layout/signature_widget.xml | 2 +- collect_app/src/main/res/layout/time_widget_answer.xml | 2 +- collect_app/src/main/res/layout/url_widget_answer.xml | 2 +- collect_app/src/main/res/layout/video_widget.xml | 6 +++--- collect_app/src/main/res/values/buttons.xml | 3 ++- collect_app/src/main/res/values/theme.xml | 2 +- material/src/main/res/values/attrs.xml | 2 +- 27 files changed, 35 insertions(+), 34 deletions(-) diff --git a/collect_app/src/main/res/layout/annotate_widget.xml b/collect_app/src/main/res/layout/annotate_widget.xml index 50bd9a965db..2a38f10fc54 100644 --- a/collect_app/src/main/res/layout/annotate_widget.xml +++ b/collect_app/src/main/res/layout/annotate_widget.xml @@ -7,7 +7,7 @@ diff --git a/collect_app/src/main/res/layout/audio_widget_answer.xml b/collect_app/src/main/res/layout/audio_widget_answer.xml index 1891f18e726..83196703e00 100644 --- a/collect_app/src/main/res/layout/audio_widget_answer.xml +++ b/collect_app/src/main/res/layout/audio_widget_answer.xml @@ -6,7 +6,7 @@ @@ -14,7 +14,7 @@ diff --git a/collect_app/src/main/res/layout/barcode_widget_answer.xml b/collect_app/src/main/res/layout/barcode_widget_answer.xml index f19e01d2663..b827ffc1d49 100644 --- a/collect_app/src/main/res/layout/barcode_widget_answer.xml +++ b/collect_app/src/main/res/layout/barcode_widget_answer.xml @@ -6,7 +6,7 @@ diff --git a/collect_app/src/main/res/layout/bearing_widget_answer.xml b/collect_app/src/main/res/layout/bearing_widget_answer.xml index fdda60b00e5..94889a2122c 100644 --- a/collect_app/src/main/res/layout/bearing_widget_answer.xml +++ b/collect_app/src/main/res/layout/bearing_widget_answer.xml @@ -6,7 +6,7 @@ diff --git a/collect_app/src/main/res/layout/date_widget_answer.xml b/collect_app/src/main/res/layout/date_widget_answer.xml index 794c6b37c94..3c87eb67450 100644 --- a/collect_app/src/main/res/layout/date_widget_answer.xml +++ b/collect_app/src/main/res/layout/date_widget_answer.xml @@ -5,7 +5,7 @@ diff --git a/collect_app/src/main/res/layout/draw_widget.xml b/collect_app/src/main/res/layout/draw_widget.xml index 29168f7a18b..0b227aac5b7 100644 --- a/collect_app/src/main/res/layout/draw_widget.xml +++ b/collect_app/src/main/res/layout/draw_widget.xml @@ -7,7 +7,7 @@ diff --git a/collect_app/src/main/res/layout/ex_audio_widget_answer.xml b/collect_app/src/main/res/layout/ex_audio_widget_answer.xml index fb8d80b4c86..4005aef3c44 100644 --- a/collect_app/src/main/res/layout/ex_audio_widget_answer.xml +++ b/collect_app/src/main/res/layout/ex_audio_widget_answer.xml @@ -6,7 +6,7 @@ diff --git a/collect_app/src/main/res/layout/ex_image_widget_answer.xml b/collect_app/src/main/res/layout/ex_image_widget_answer.xml index 78366dea308..895343e8ea9 100644 --- a/collect_app/src/main/res/layout/ex_image_widget_answer.xml +++ b/collect_app/src/main/res/layout/ex_image_widget_answer.xml @@ -6,7 +6,7 @@ diff --git a/collect_app/src/main/res/layout/ex_printer_widget.xml b/collect_app/src/main/res/layout/ex_printer_widget.xml index efdcc6a797b..fb438fd6659 100644 --- a/collect_app/src/main/res/layout/ex_printer_widget.xml +++ b/collect_app/src/main/res/layout/ex_printer_widget.xml @@ -6,7 +6,7 @@ diff --git a/collect_app/src/main/res/layout/image_widget.xml b/collect_app/src/main/res/layout/image_widget.xml index 5ad1347892e..b69d65a1a85 100644 --- a/collect_app/src/main/res/layout/image_widget.xml +++ b/collect_app/src/main/res/layout/image_widget.xml @@ -7,7 +7,7 @@ diff --git a/collect_app/src/main/res/layout/printer_widget.xml b/collect_app/src/main/res/layout/printer_widget.xml index f78a958f652..cf02c5ce897 100644 --- a/collect_app/src/main/res/layout/printer_widget.xml +++ b/collect_app/src/main/res/layout/printer_widget.xml @@ -6,7 +6,7 @@ diff --git a/collect_app/src/main/res/layout/ranking_widget.xml b/collect_app/src/main/res/layout/ranking_widget.xml index 7bbeb27e1e8..200a48387f1 100644 --- a/collect_app/src/main/res/layout/ranking_widget.xml +++ b/collect_app/src/main/res/layout/ranking_widget.xml @@ -7,7 +7,7 @@ diff --git a/collect_app/src/main/res/layout/url_widget_answer.xml b/collect_app/src/main/res/layout/url_widget_answer.xml index 4b95df2f5a6..e38cadcb6fe 100644 --- a/collect_app/src/main/res/layout/url_widget_answer.xml +++ b/collect_app/src/main/res/layout/url_widget_answer.xml @@ -5,7 +5,7 @@ diff --git a/collect_app/src/main/res/layout/video_widget.xml b/collect_app/src/main/res/layout/video_widget.xml index 8ae1a26590f..9473a321fd1 100644 --- a/collect_app/src/main/res/layout/video_widget.xml +++ b/collect_app/src/main/res/layout/video_widget.xml @@ -6,7 +6,7 @@ - + + diff --git a/maps/src/main/res/layout/offline_map_layers_picker.xml b/maps/src/main/res/layout/offline_map_layers_picker.xml index e5ca1e2e804..057570e847e 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker.xml @@ -99,6 +99,7 @@ android:layout_width="match_parent" android:layout_height="1dp" android:background="@drawable/list_item_divider" + android:layout_marginBottom="@dimen/margin_extra_extra_small" app:layout_constraintBottom_toTopOf="@id/save" /> + android:layout_marginBottom="@dimen/margin_extra_extra_small" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@id/mbtiles_info_icon_text" /> Date: Thu, 23 May 2024 16:03:27 +0200 Subject: [PATCH 510/750] Get rid of OfflineMapLayersPickerViewModel.Factory --- .../collect/android/application/Collect.java | 3 ++- .../injection/config/AppDependencyModule.java | 6 ----- .../config/CollectGeoDependencyModule.kt | 14 +++++++---- .../screens/MapsPreferencesFragment.kt | 6 ++--- .../SelectOneFromMapDialogFragmentTest.kt | 4 ++-- .../items/SelectOneFromMapWidgetTest.kt | 2 +- geo/build.gradle.kts | 1 + .../java/org/odk/collect/geo/DaggerSetup.kt | 10 ++++++-- .../geo/geopoint/GeoPointMapActivity.java | 14 ++++++++--- .../collect/geo/geopoly/GeoPolyActivity.java | 18 ++++++++++---- .../geo/selection/SelectionMapFragment.kt | 14 ++++++++--- .../geo/geopoint/GeoPointMapActivityTest.java | 18 ++++++++++++-- .../geo/geopoly/GeoPolyActivityTest.kt | 14 +++++++++-- .../geo/selection/SelectionMapFragmentTest.kt | 24 ++++++++++--------- .../maps/layers/OfflineMapLayersPicker.kt | 13 ++++++++-- .../layers/OfflineMapLayersPickerViewModel.kt | 13 ---------- .../maps/layers/OfflineMapLayersPickerTest.kt | 3 +-- 17 files changed, 115 insertions(+), 62 deletions(-) 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 86f452a1fd1..602451ffeb0 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 @@ -305,7 +305,8 @@ public GeoDependencyComponent getGeoDependencyComponent() { applicationComponent.locationClient(), applicationComponent.scheduler(), applicationComponent.permissionsChecker(), - applicationComponent.offlineMapLayersPickerViewModelFactory(), + applicationComponent.referenceLayerRepository(), + applicationComponent.settingsProvider(), applicationComponent.externalWebPageHelper() )) .build(); 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 3f555c686fd..984c007bb61 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 @@ -112,7 +112,6 @@ import org.odk.collect.location.LocationClientProvider; import org.odk.collect.maps.MapFragmentFactory; import org.odk.collect.maps.layers.DirectoryReferenceLayerRepository; -import org.odk.collect.maps.layers.OfflineMapLayersPickerViewModel; import org.odk.collect.maps.layers.ReferenceLayerRepository; import org.odk.collect.metadata.InstallIDProvider; import org.odk.collect.metadata.PropertyManager; @@ -632,9 +631,4 @@ public ImageCompressionController providesImageCompressorManager() { public FormLoaderTask.FormEntryControllerFactory formEntryControllerFactory(SettingsProvider settingsProvider) { return new CollectFormEntryControllerFactory(); } - - @Provides - public OfflineMapLayersPickerViewModel.Factory providesOfflineMapLayersPickerViewModelFactory(ReferenceLayerRepository referenceLayerRepository, Scheduler scheduler, SettingsProvider settingsProvider) { - return new OfflineMapLayersPickerViewModel.Factory(referenceLayerRepository, scheduler, settingsProvider); - } } diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGeoDependencyModule.kt b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGeoDependencyModule.kt index 8a86cecde5c..58c0883eb07 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGeoDependencyModule.kt +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGeoDependencyModule.kt @@ -11,8 +11,9 @@ import org.odk.collect.location.satellites.SatelliteInfoClient import org.odk.collect.location.tracker.ForegroundServiceLocationTracker import org.odk.collect.location.tracker.LocationTracker import org.odk.collect.maps.MapFragmentFactory -import org.odk.collect.maps.layers.OfflineMapLayersPickerViewModel +import org.odk.collect.maps.layers.ReferenceLayerRepository import org.odk.collect.permissions.PermissionsChecker +import org.odk.collect.settings.SettingsProvider import org.odk.collect.webpage.ExternalWebPageHelper class CollectGeoDependencyModule( @@ -20,7 +21,8 @@ class CollectGeoDependencyModule( private val locationClient: LocationClient, private val scheduler: Scheduler, private val permissionChecker: PermissionsChecker, - private val offlineMapLayersPickerViewModelFactory: OfflineMapLayersPickerViewModel.Factory, + private val referenceLayerRepository: ReferenceLayerRepository, + private val settingsProvider: SettingsProvider, private val externalWebPageHelper: ExternalWebPageHelper ) : GeoDependencyModule() { @@ -50,8 +52,12 @@ class CollectGeoDependencyModule( return permissionChecker } - override fun providesOfflineMapLayersPickerViewModelFactory(): OfflineMapLayersPickerViewModel.Factory { - return offlineMapLayersPickerViewModelFactory + override fun providesReferenceLayerRepository(): ReferenceLayerRepository { + return referenceLayerRepository + } + + override fun providesSettingsProvider(): SettingsProvider { + return settingsProvider } override fun providesExternalWebPageHelper(): ExternalWebPageHelper { diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt index d8d7dd98553..a20dc55e805 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt @@ -25,9 +25,9 @@ import org.odk.collect.androidshared.ui.DialogFragmentUtils import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.androidshared.ui.PrefUtils import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard.allowClick +import org.odk.collect.async.Scheduler import org.odk.collect.maps.MapConfigurator import org.odk.collect.maps.layers.OfflineMapLayersPicker -import org.odk.collect.maps.layers.OfflineMapLayersPickerViewModel import org.odk.collect.maps.layers.ReferenceLayerRepository import org.odk.collect.settings.keys.ProjectKeys.CATEGORY_BASEMAP import org.odk.collect.settings.keys.ProjectKeys.KEY_BASEMAP_SOURCE @@ -43,7 +43,7 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP lateinit var referenceLayerRepository: ReferenceLayerRepository @Inject - lateinit var viewModelFactory: OfflineMapLayersPickerViewModel.Factory + lateinit var scheduler: Scheduler @Inject lateinit var externalWebPageHelper: ExternalWebPageHelper @@ -56,7 +56,7 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP override fun onCreate(savedInstanceState: Bundle?) { childFragmentManager.fragmentFactory = FragmentFactoryBuilder() .forClass(OfflineMapLayersPicker::class) { - OfflineMapLayersPicker(viewModelFactory, externalWebPageHelper) + OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) } .build() diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneFromMapDialogFragmentTest.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneFromMapDialogFragmentTest.kt index 1db340e1b11..c53e9bae76f 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneFromMapDialogFragmentTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneFromMapDialogFragmentTest.kt @@ -114,8 +114,8 @@ class SelectOneFromMapDialogFragmentTest { } override fun providesReferenceLayerRepository( - storagePathProvider: StoragePathProvider?, - settingsProvider: SettingsProvider? + storagePathProvider: StoragePathProvider, + settingsProvider: SettingsProvider ): ReferenceLayerRepository { return mock() } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneFromMapWidgetTest.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneFromMapWidgetTest.kt index f6bd4bf1f73..b76851ecf46 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneFromMapWidgetTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneFromMapWidgetTest.kt @@ -79,7 +79,7 @@ class SelectOneFromMapWidgetTest { } override fun providesReferenceLayerRepository( - storagePathProvider: StoragePathProvider?, + storagePathProvider: StoragePathProvider, settingsProvider: SettingsProvider ): ReferenceLayerRepository { return mock() diff --git a/geo/build.gradle.kts b/geo/build.gradle.kts index e1bd7e0fee5..b7f7abb17eb 100644 --- a/geo/build.gradle.kts +++ b/geo/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation(project(":async")) implementation(project(":analytics")) implementation(project(":permissions")) + implementation(project(":settings")) implementation(project(":maps")) implementation(project(":material")) implementation(project(":web-page")) diff --git a/geo/src/main/java/org/odk/collect/geo/DaggerSetup.kt b/geo/src/main/java/org/odk/collect/geo/DaggerSetup.kt index abad311c6c4..0ff45d1b808 100644 --- a/geo/src/main/java/org/odk/collect/geo/DaggerSetup.kt +++ b/geo/src/main/java/org/odk/collect/geo/DaggerSetup.kt @@ -19,8 +19,9 @@ import org.odk.collect.location.LocationClient import org.odk.collect.location.satellites.SatelliteInfoClient import org.odk.collect.location.tracker.LocationTracker import org.odk.collect.maps.MapFragmentFactory -import org.odk.collect.maps.layers.OfflineMapLayersPickerViewModel +import org.odk.collect.maps.layers.ReferenceLayerRepository import org.odk.collect.permissions.PermissionsChecker +import org.odk.collect.settings.SettingsProvider import org.odk.collect.webpage.ExternalWebPageHelper import javax.inject.Singleton @@ -110,7 +111,12 @@ open class GeoDependencyModule { } @Provides - open fun providesOfflineMapLayersPickerViewModelFactory(): OfflineMapLayersPickerViewModel.Factory { + open fun providesReferenceLayerRepository(): ReferenceLayerRepository { + throw UnsupportedOperationException("This should be overridden by dependent application") + } + + @Provides + open fun providesSettingsProvider(): SettingsProvider { throw UnsupportedOperationException("This should be overridden by dependent application") } diff --git a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java index 6fd052a8757..b731fc95187 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java +++ b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java @@ -35,6 +35,7 @@ import org.odk.collect.androidshared.ui.DialogFragmentUtils; import org.odk.collect.androidshared.ui.FragmentFactoryBuilder; import org.odk.collect.androidshared.ui.ToastUtils; +import org.odk.collect.async.Scheduler; import org.odk.collect.externalapp.ExternalAppUtils; import org.odk.collect.geo.GeoDependencyComponentProvider; import org.odk.collect.geo.GeoUtils; @@ -43,9 +44,10 @@ import org.odk.collect.maps.MapFragmentFactory; import org.odk.collect.maps.MapPoint; import org.odk.collect.maps.layers.OfflineMapLayersPicker; -import org.odk.collect.maps.layers.OfflineMapLayersPickerViewModel; +import org.odk.collect.maps.layers.ReferenceLayerRepository; import org.odk.collect.maps.markers.MarkerDescription; import org.odk.collect.maps.markers.MarkerIconDescription; +import org.odk.collect.settings.SettingsProvider; import org.odk.collect.strings.localization.LocalizedActivity; import org.odk.collect.webpage.ExternalWebPageHelper; @@ -87,7 +89,13 @@ public class GeoPointMapActivity extends LocalizedActivity { MapFragmentFactory mapFragmentFactory; @Inject - OfflineMapLayersPickerViewModel.Factory viewModelFactory; + ReferenceLayerRepository referenceLayerRepository; + + @Inject + Scheduler scheduler; + + @Inject + SettingsProvider settingsProvider; @Inject ExternalWebPageHelper externalWebPageHelper; @@ -133,7 +141,7 @@ public void onCreate(Bundle savedInstanceState) { getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() .forClass(MapFragment.class, () -> (Fragment) mapFragmentFactory.createMapFragment()) - .forClass(OfflineMapLayersPicker.class, () -> new OfflineMapLayersPicker(viewModelFactory, externalWebPageHelper)) + .forClass(OfflineMapLayersPicker.class, () -> new OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper)) .build() ); diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java index 608bdbf6938..634377fdf43 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java @@ -35,6 +35,7 @@ import org.odk.collect.androidshared.ui.DialogFragmentUtils; import org.odk.collect.androidshared.ui.FragmentFactoryBuilder; import org.odk.collect.androidshared.ui.ToastUtils; +import org.odk.collect.async.Scheduler; import org.odk.collect.externalapp.ExternalAppUtils; import org.odk.collect.geo.Constants; import org.odk.collect.geo.GeoDependencyComponentProvider; @@ -48,7 +49,8 @@ import org.odk.collect.maps.MapFragmentFactory; import org.odk.collect.maps.MapPoint; import org.odk.collect.maps.layers.OfflineMapLayersPicker; -import org.odk.collect.maps.layers.OfflineMapLayersPickerViewModel; +import org.odk.collect.maps.layers.ReferenceLayerRepository; +import org.odk.collect.settings.SettingsProvider; import org.odk.collect.strings.localization.LocalizedActivity; import org.odk.collect.webpage.ExternalWebPageHelper; @@ -74,7 +76,7 @@ public class GeoPolyActivity extends LocalizedActivity implements GeoPolySetting public enum OutputMode { GEOTRACE, GEOSHAPE } - private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final ScheduledExecutorService executorServiceScheduler = Executors.newSingleThreadScheduledExecutor(); private ScheduledFuture schedulerHandler; private OutputMode outputMode; @@ -86,7 +88,13 @@ public enum OutputMode { GEOTRACE, GEOSHAPE } LocationTracker locationTracker; @Inject - OfflineMapLayersPickerViewModel.Factory viewModelFactory; + ReferenceLayerRepository referenceLayerRepository; + + @Inject + Scheduler scheduler; + + @Inject + SettingsProvider settingsProvider; @Inject ExternalWebPageHelper externalWebPageHelper; @@ -146,7 +154,7 @@ public void handleOnBackPressed() { getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() .forClass(MapFragment.class, () -> (Fragment) mapFragmentFactory.createMapFragment()) - .forClass(OfflineMapLayersPicker.class, () -> new OfflineMapLayersPicker(viewModelFactory, externalWebPageHelper)) + .forClass(OfflineMapLayersPicker.class, () -> new OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper)) .build() ); @@ -342,7 +350,7 @@ public void startInput() { locationTracker.start(retainMockAccuracy); recordPoint(map.getGpsLocation()); - schedulerHandler = scheduler.scheduleAtFixedRate(() -> runOnUiThread(() -> { + schedulerHandler = executorServiceScheduler.scheduleAtFixedRate(() -> runOnUiThread(() -> { Location currentLocation = locationTracker.getCurrentLocation(); if (currentLocation != null) { diff --git a/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt b/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt index 9a6114f70ed..3ddb596d2f7 100644 --- a/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt @@ -22,6 +22,7 @@ import org.odk.collect.androidshared.ui.DialogFragmentUtils import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.androidshared.ui.ToastUtils import org.odk.collect.androidshared.ui.multiclicksafe.setMultiClickSafeOnClickListener +import org.odk.collect.async.Scheduler import org.odk.collect.geo.GeoDependencyComponentProvider import org.odk.collect.geo.databinding.SelectionMapLayoutBinding import org.odk.collect.maps.LineDescription @@ -30,12 +31,13 @@ import org.odk.collect.maps.MapFragmentFactory import org.odk.collect.maps.MapPoint import org.odk.collect.maps.PolygonDescription import org.odk.collect.maps.layers.OfflineMapLayersPicker -import org.odk.collect.maps.layers.OfflineMapLayersPickerViewModel +import org.odk.collect.maps.layers.ReferenceLayerRepository import org.odk.collect.maps.markers.MarkerDescription import org.odk.collect.maps.markers.MarkerIconDescription import org.odk.collect.material.BottomSheetBehavior import org.odk.collect.material.MaterialProgressDialogFragment import org.odk.collect.permissions.PermissionsChecker +import org.odk.collect.settings.SettingsProvider import org.odk.collect.webpage.ExternalWebPageHelper import javax.inject.Inject @@ -58,7 +60,13 @@ class SelectionMapFragment( lateinit var permissionsChecker: PermissionsChecker @Inject - lateinit var viewModelFactory: OfflineMapLayersPickerViewModel.Factory + lateinit var referenceLayerRepository: ReferenceLayerRepository + + @Inject + lateinit var scheduler: Scheduler + + @Inject + lateinit var settingsProvider: SettingsProvider @Inject lateinit var externalWebPageHelper: ExternalWebPageHelper @@ -89,7 +97,7 @@ class SelectionMapFragment( mapFragmentFactory.createMapFragment() as Fragment } .forClass(OfflineMapLayersPicker::class) { - OfflineMapLayersPicker(viewModelFactory, externalWebPageHelper) + OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) } .build() diff --git a/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.java b/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.java index 13abb8cf4c1..21b967e0312 100644 --- a/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.java +++ b/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.java @@ -22,6 +22,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.odk.collect.androidtest.ActivityScenarioLauncherRule; +import org.odk.collect.async.Scheduler; import org.odk.collect.externalapp.ExternalAppUtils; import org.odk.collect.geo.DaggerGeoDependencyComponent; import org.odk.collect.geo.GeoDependencyModule; @@ -30,7 +31,8 @@ import org.odk.collect.geo.support.RobolectricApplication; import org.odk.collect.maps.MapFragmentFactory; import org.odk.collect.maps.MapPoint; -import org.odk.collect.maps.layers.OfflineMapLayersPickerViewModel; +import org.odk.collect.maps.layers.ReferenceLayerRepository; +import org.odk.collect.settings.SettingsProvider; import org.odk.collect.webpage.ExternalWebPageHelper; import org.robolectric.shadows.ShadowApplication; @@ -62,7 +64,19 @@ public MapFragmentFactory providesMapFragmentFactory() { @NonNull @Override - public OfflineMapLayersPickerViewModel.Factory providesOfflineMapLayersPickerViewModelFactory() { + public ReferenceLayerRepository providesReferenceLayerRepository() { + return mock(); + } + + @NonNull + @Override + public Scheduler providesScheduler() { + return mock(); + } + + @NonNull + @Override + public SettingsProvider providesSettingsProvider() { return mock(); } diff --git a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyActivityTest.kt b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyActivityTest.kt index c51469b8178..dc4d62d4f94 100644 --- a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyActivityTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyActivityTest.kt @@ -25,6 +25,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.odk.collect.androidtest.ActivityScenarioExtensions.isFinishing import org.odk.collect.androidtest.ActivityScenarioLauncherRule +import org.odk.collect.async.Scheduler import org.odk.collect.geo.Constants import org.odk.collect.geo.DaggerGeoDependencyComponent import org.odk.collect.geo.GeoDependencyModule @@ -35,7 +36,8 @@ import org.odk.collect.location.tracker.LocationTracker import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapFragmentFactory import org.odk.collect.maps.MapPoint -import org.odk.collect.maps.layers.OfflineMapLayersPickerViewModel +import org.odk.collect.maps.layers.ReferenceLayerRepository +import org.odk.collect.settings.SettingsProvider import org.odk.collect.webpage.ExternalWebPageHelper import org.robolectric.Shadows @@ -68,7 +70,15 @@ class GeoPolyActivityTest { return locationTracker } - override fun providesOfflineMapLayersPickerViewModelFactory(): OfflineMapLayersPickerViewModel.Factory { + override fun providesReferenceLayerRepository(): ReferenceLayerRepository { + return mock() + } + + override fun providesScheduler(): Scheduler { + return mock() + } + + override fun providesSettingsProvider(): SettingsProvider { return mock() } diff --git a/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt b/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt index 6aa98f261f3..0aec6ff8b13 100644 --- a/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt @@ -8,7 +8,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentFactory import androidx.lifecycle.Lifecycle import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click @@ -36,6 +35,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.odk.collect.androidshared.livedata.MutableNonNullLiveData import org.odk.collect.androidshared.ui.FragmentFactoryBuilder +import org.odk.collect.async.Scheduler import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule import org.odk.collect.geo.DaggerGeoDependencyComponent import org.odk.collect.geo.GeoDependencyModule @@ -47,10 +47,11 @@ import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapFragmentFactory import org.odk.collect.maps.MapPoint import org.odk.collect.maps.layers.OfflineMapLayersPicker -import org.odk.collect.maps.layers.OfflineMapLayersPickerViewModel +import org.odk.collect.maps.layers.ReferenceLayerRepository import org.odk.collect.material.BottomSheetBehavior import org.odk.collect.material.MaterialProgressDialogFragment import org.odk.collect.permissions.PermissionsChecker +import org.odk.collect.settings.SettingsProvider import org.odk.collect.testshared.RobolectricHelpers.getFragmentByClass import org.odk.collect.webpage.ExternalWebPageHelper @@ -102,15 +103,16 @@ class SelectionMapFragmentTest { } } - override fun providesOfflineMapLayersPickerViewModelFactory(): OfflineMapLayersPickerViewModel.Factory { - val viewModel = mock().also { - whenever(it.data).thenReturn(MutableLiveData(Pair(emptyList(), null))) - } - return object : OfflineMapLayersPickerViewModel.Factory(mock(), mock(), mock()) { - override fun create(modelClass: Class): T { - return viewModel as T - } - } + override fun providesReferenceLayerRepository(): ReferenceLayerRepository { + return mock() + } + + override fun providesScheduler(): Scheduler { + return mock() + } + + override fun providesSettingsProvider(): SettingsProvider { + return mock() } override fun providesExternalWebPageHelper(): ExternalWebPageHelper { diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index cbb11a6a95b..f7ca427929b 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -6,19 +6,28 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.odk.collect.androidshared.ui.GroupClickListener.addOnClickListener +import org.odk.collect.async.Scheduler import org.odk.collect.maps.databinding.OfflineMapLayersPickerBinding +import org.odk.collect.settings.SettingsProvider import org.odk.collect.webpage.ExternalWebPageHelper class OfflineMapLayersPicker( - private val viewModelFactory: ViewModelProvider.Factory, + private val referenceLayerRepository: ReferenceLayerRepository, + private val scheduler: Scheduler, + private val settingsProvider: SettingsProvider, private val externalWebPageHelper: ExternalWebPageHelper ) : BottomSheetDialogFragment() { private val viewModel: OfflineMapLayersPickerViewModel by viewModels { - viewModelFactory + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return OfflineMapLayersPickerViewModel(referenceLayerRepository, scheduler, settingsProvider) as T + } + } } private lateinit var offlineMapLayersPickerBinding: OfflineMapLayersPickerBinding diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt index 0ef1e342eb3..ae08dcbd452 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt @@ -3,7 +3,6 @@ package org.odk.collect.maps.layers import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import org.odk.collect.async.Scheduler import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.keys.ProjectKeys @@ -36,16 +35,4 @@ class OfflineMapLayersPickerViewModel( fun changeSelectedLayerId(selectedLayerId: String?) { _data.postValue(_data.value?.copy(second = selectedLayerId)) } - - open class Factory( - private val referenceLayerRepository: ReferenceLayerRepository, - private val scheduler: Scheduler, - private val settingsProvider: SettingsProvider - ) : ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return OfflineMapLayersPickerViewModel(referenceLayerRepository, scheduler, settingsProvider) as T - } - } } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index 7963484eb35..0598e7a548a 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -43,14 +43,13 @@ class OfflineMapLayersPickerTest { } private val scheduler = FakeScheduler() private val settingsProvider = InMemSettingsProvider() - private val viewModelFactory = OfflineMapLayersPickerViewModel.Factory(referenceLayerRepository, scheduler, settingsProvider) private val externalWebPageHelper = mock() @get:Rule val fragmentScenarioLauncherRule = FragmentScenarioLauncherRule( FragmentFactoryBuilder() .forClass(OfflineMapLayersPicker::class) { - OfflineMapLayersPicker(viewModelFactory, externalWebPageHelper) + OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) }.build() ) From a1945243e69f927c2a39c67bdc357065984ad8c3 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 24 May 2024 11:14:25 +0200 Subject: [PATCH 511/750] Improved managing margins/paddings in the map layers picker --- .../src/main/res/layout/offline_map_layer.xml | 2 +- .../res/layout/offline_map_layers_picker.xml | 34 ++++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/maps/src/main/res/layout/offline_map_layer.xml b/maps/src/main/res/layout/offline_map_layer.xml index 5037ca0d4fd..ba41f164492 100644 --- a/maps/src/main/res/layout/offline_map_layer.xml +++ b/maps/src/main/res/layout/offline_map_layer.xml @@ -4,7 +4,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingHorizontal="@dimen/margin_small" android:paddingVertical="@dimen/margin_extra_small"> + + + + @@ -53,7 +67,7 @@ android:textAppearance="?textAppearanceLabelLarge" android:textColor="?colorAccent" app:layout_constraintBottom_toBottomOf="@id/mbtiles_info_icon" - app:layout_constraintEnd_toEndOf="@id/message" + app:layout_constraintEnd_toStartOf="@id/guideline_end" app:layout_constraintStart_toEndOf="@id/mbtiles_info_icon" app:layout_constraintTop_toTopOf="@id/mbtiles_info_icon" /> @@ -74,10 +88,12 @@ + app:layout_constraintEnd_toStartOf="@id/guideline_end" /> Date: Mon, 13 May 2024 15:35:09 +0100 Subject: [PATCH 512/750] Don't try and save entities with null ID --- .../feature/formentry/EntityFormTest.kt | 19 +++++++++++ .../android/formentry/FormEntryUseCases.kt | 13 ++++---- .../formmanagement/ServerFormUseCases.kt | 2 +- .../android/javarosawrapper/FormController.kt | 5 ++- .../JavaRosaFormController.java | 13 ++------ .../android/utilities/StubFormController.kt | 5 ++- .../collect/entities/LocalEntityUseCases.kt | 18 +++++++++-- .../entities/LocalEntityUseCasesTest.kt | 32 +++++++++---------- 8 files changed, 65 insertions(+), 42 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/EntityFormTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/EntityFormTest.kt index 1a6dd91bfec..680557fd671 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/EntityFormTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/EntityFormTest.kt @@ -108,6 +108,25 @@ class EntityFormTest { .assertTextDoesNotExist("Roman Roy") } + @Test + fun fillingEntityUpdateForm_withoutSelectingEntity_doesNothing() { + testDependencies.server.addForm( + "one-question-entity-update.xml", + listOf(EntityListItem("people.csv")) + ) + + rule.withMatchExactlyProject(testDependencies.server.url) + .setupEntities("people") + + .startBlankForm("One Question Entity Update") + .swipeToNextQuestion("Name") + .swipeToEndScreen() + .clickFinalize() + + .startBlankForm("One Question Entity Update") + .assertText("Roman Roy") + } + @Test fun entityListSecondaryInstancesAreConsistentBetweenFollowUpForms() { testDependencies.server.apply { diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryUseCases.kt b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryUseCases.kt index 691fc17adcb..2899260f7a5 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryUseCases.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryUseCases.kt @@ -16,6 +16,7 @@ import org.odk.collect.android.javarosawrapper.JavaRosaFormController import org.odk.collect.android.utilities.FileUtils import org.odk.collect.android.utilities.FormUtils import org.odk.collect.entities.EntitiesRepository +import org.odk.collect.entities.LocalEntityUseCases import org.odk.collect.forms.Form import org.odk.collect.forms.FormsRepository import org.odk.collect.forms.instances.Instance @@ -31,7 +32,8 @@ object FormEntryUseCases { formDefCache: FormDefCache ): Pair? { val form = - formsRepository.getAllByFormIdAndVersion(instance.formId, instance.formVersion).firstOrNull() + formsRepository.getAllByFormIdAndVersion(instance.formId, instance.formVersion) + .firstOrNull() return if (form == null) { null } else { @@ -159,11 +161,10 @@ object FormEntryUseCases { entitiesRepository: EntitiesRepository ) { formController.finalizeForm() - formController.getEntities().forEach { entity -> - if (entitiesRepository.getLists().contains(entity.list)) { - entitiesRepository.save(entity) - } - } + LocalEntityUseCases.updateLocalEntitiesFromForm( + formController.getEntities(), + entitiesRepository + ) } private fun getInstanceFromFormController( 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 a5b5cd94d15..53480f30d30 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 @@ -112,7 +112,7 @@ object ServerFormUseCases { val dataset = mediaFile.filename.substringBefore(".csv") if (entitiesRepository.getLists().contains(dataset)) { - LocalEntityUseCases.updateLocalEntities(dataset, tempMediaFile, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer(dataset, tempMediaFile, entitiesRepository) } } diff --git a/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormController.kt b/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormController.kt index 641fa401ebc..97b4dd63009 100644 --- a/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormController.kt +++ b/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormController.kt @@ -5,14 +5,13 @@ import org.javarosa.core.model.FormIndex import org.javarosa.core.model.data.IAnswerData import org.javarosa.core.model.instance.TreeReference import org.javarosa.core.services.transport.payload.ByteArrayPayload +import org.javarosa.entities.internal.Entities import org.javarosa.form.api.FormEntryCaption import org.javarosa.form.api.FormEntryPrompt import org.odk.collect.android.exception.JavaRosaException import org.odk.collect.android.formentry.audit.AuditEventLogger -import org.odk.collect.entities.Entity import java.io.File import java.io.IOException -import java.util.stream.Stream interface FormController { fun getFormDef(): FormDef? @@ -335,5 +334,5 @@ interface FormController { fun getAnswer(treeReference: TreeReference?): IAnswerData? - fun getEntities(): Stream + fun getEntities(): Entities? } diff --git a/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/JavaRosaFormController.java b/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/JavaRosaFormController.java index ac0e713d23e..457e81a7934 100644 --- a/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/JavaRosaFormController.java +++ b/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/JavaRosaFormController.java @@ -47,14 +47,13 @@ import org.javarosa.xform.parse.XFormParser; import org.javarosa.xpath.XPathParseTool; import org.javarosa.xpath.expr.XPathExpression; -import org.odk.collect.android.exception.JavaRosaException; import org.odk.collect.android.dynamicpreload.ExternalDataUtil; +import org.odk.collect.android.exception.JavaRosaException; import org.odk.collect.android.formentry.audit.AsyncTaskAuditEventWriter; import org.odk.collect.android.formentry.audit.AuditConfig; import org.odk.collect.android.formentry.audit.AuditEventLogger; import org.odk.collect.android.utilities.Appearances; import org.odk.collect.android.utilities.FileUtils; -import org.odk.collect.entities.Entity; import java.io.File; import java.io.IOException; @@ -62,7 +61,6 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.stream.Stream; import timber.log.Timber; @@ -1111,12 +1109,7 @@ public IAnswerData getAnswer(TreeReference treeReference) { return getFormDef().getMainInstance().resolveReference(treeReference).getValue(); } - public Stream getEntities() { - Entities extra = formEntryController.getModel().getExtras().get(Entities.class); - if (extra != null) { - return extra.getEntities().stream().map(entity -> new Entity(entity.dataset, entity.id, entity.label, entity.version, entity.properties)); - } else { - return Stream.empty(); - } + public Entities getEntities() { + return formEntryController.getModel().getExtras().get(Entities.class); } } diff --git a/collect_app/src/test/java/org/odk/collect/android/utilities/StubFormController.kt b/collect_app/src/test/java/org/odk/collect/android/utilities/StubFormController.kt index 5e7ac9f0d0c..06dd75f7418 100644 --- a/collect_app/src/test/java/org/odk/collect/android/utilities/StubFormController.kt +++ b/collect_app/src/test/java/org/odk/collect/android/utilities/StubFormController.kt @@ -5,6 +5,7 @@ import org.javarosa.core.model.FormIndex import org.javarosa.core.model.data.IAnswerData import org.javarosa.core.model.instance.TreeReference import org.javarosa.core.services.transport.payload.ByteArrayPayload +import org.javarosa.entities.internal.Entities import org.javarosa.form.api.FormEntryCaption import org.javarosa.form.api.FormEntryPrompt import org.odk.collect.android.exception.JavaRosaException @@ -13,9 +14,7 @@ import org.odk.collect.android.javarosawrapper.FormController import org.odk.collect.android.javarosawrapper.InstanceMetadata import org.odk.collect.android.javarosawrapper.SuccessValidationResult import org.odk.collect.android.javarosawrapper.ValidationResult -import org.odk.collect.entities.Entity import java.io.File -import java.util.stream.Stream open class StubFormController : FormController { override fun getFormDef(): FormDef? = null @@ -157,5 +156,5 @@ open class StubFormController : FormController { override fun getAnswer(treeReference: TreeReference?): IAnswerData? = null - override fun getEntities(): Stream = Stream.empty() + override fun getEntities(): Entities? = null } diff --git a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt index ab7e206bba4..7e667b5786e 100644 --- a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt +++ b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt @@ -2,17 +2,29 @@ package org.odk.collect.entities import org.javarosa.core.model.instance.CsvExternalInstance import org.javarosa.core.model.instance.TreeElement +import org.javarosa.entities.internal.Entities import java.io.File object LocalEntityUseCases { - fun updateLocalEntities( + @JvmStatic + fun updateLocalEntitiesFromForm(entities: Entities?, entitiesRepository: EntitiesRepository) { + entities?.entities?.forEach { + val id = it.id + if (id != null && entitiesRepository.getLists().contains(it.dataset)) { + val entity = Entity(it.dataset, id, it.label, it.version, it.properties) + entitiesRepository.save(entity) + } + } + } + + fun updateLocalEntitiesFromServer( list: String, - onlineList: File, + serverList: File, entitiesRepository: EntitiesRepository ) { val root = try { - CsvExternalInstance().parse(list, onlineList.absolutePath) + CsvExternalInstance().parse(list, serverList.absolutePath) } catch (e: Exception) { return } diff --git a/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt b/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt index 814bd0c8ff8..5143ac44126 100644 --- a/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt +++ b/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt @@ -19,7 +19,7 @@ class LocalEntityUseCasesTest { entitiesRepository.save(Entity("songs", "noah", "Noa", 1)) val csv = createEntityList(Entity("songs", "noah", "Noah", 2)) - LocalEntityUseCases.updateLocalEntities("songs", csv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", csv, entitiesRepository) val songs = entitiesRepository.getEntities("songs") assertThat(songs, containsInAnyOrder(Entity("songs", "noah", "Noah", 2, state = ONLINE))) } @@ -29,7 +29,7 @@ class LocalEntityUseCasesTest { entitiesRepository.save(Entity("songs", "noah", "Noah", 2)) val csv = createEntityList(Entity("songs", "noah", "Noa", 1)) - LocalEntityUseCases.updateLocalEntities("songs", csv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", csv, entitiesRepository) val songs = entitiesRepository.getEntities("songs") assertThat(songs, containsInAnyOrder(Entity("songs", "noah", "Noah", 2, state = ONLINE))) } @@ -39,7 +39,7 @@ class LocalEntityUseCasesTest { entitiesRepository.save(Entity("songs", "noah", "Noah", 2)) val csv = createEntityList(Entity("songs", "noah", "Noa", 2)) - LocalEntityUseCases.updateLocalEntities("songs", csv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", csv, entitiesRepository) val songs = entitiesRepository.getEntities("songs") assertThat(songs, containsInAnyOrder(Entity("songs", "noah", "Noah", 2, state = ONLINE))) } @@ -50,7 +50,7 @@ class LocalEntityUseCasesTest { val csv = createEntityList(Entity("songs", "noah", "Noah", 2, listOf(Pair("length", "6:38")))) - LocalEntityUseCases.updateLocalEntities("songs", csv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", csv, entitiesRepository) val songs = entitiesRepository.getEntities("songs") assertThat( songs, @@ -64,7 +64,7 @@ class LocalEntityUseCasesTest { val csv = createEntityList(Entity("songs", "noah", "Noah", 2, listOf(Pair("length", "4:58")))) - LocalEntityUseCases.updateLocalEntities("songs", csv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", csv, entitiesRepository) val songs = entitiesRepository.getEntities("songs") assertThat( songs, @@ -89,7 +89,7 @@ class LocalEntityUseCasesTest { listOf("grisaille", "Grisaille") ) - LocalEntityUseCases.updateLocalEntities("songs", csv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", csv, entitiesRepository) assertThat(entitiesRepository.getLists().size, equalTo(0)) } @@ -101,7 +101,7 @@ class LocalEntityUseCasesTest { listOf("Grisaille", "2") ) - LocalEntityUseCases.updateLocalEntities("songs", csv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", csv, entitiesRepository) assertThat(entitiesRepository.getLists().size, equalTo(0)) } @@ -113,7 +113,7 @@ class LocalEntityUseCasesTest { listOf("grisaille", "2") ) - LocalEntityUseCases.updateLocalEntities("songs", csv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", csv, entitiesRepository) assertThat(entitiesRepository.getLists().size, equalTo(0)) } @@ -121,7 +121,7 @@ class LocalEntityUseCasesTest { fun `updateLocalEntities adds online entity when its label is blank`() { val csv = createEntityList(Entity("songs", "cathedrals", label = "")) - LocalEntityUseCases.updateLocalEntities("songs", csv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", csv, entitiesRepository) val songs = entitiesRepository.getEntities("songs") assertThat( songs, @@ -133,7 +133,7 @@ class LocalEntityUseCasesTest { fun `updateLocalEntities does nothing if passed a non-CSV file`() { val file = TempFiles.createTempFile(".xml") - LocalEntityUseCases.updateLocalEntities("songs", file, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", file, entitiesRepository) assertThat(entitiesRepository.getLists().size, equalTo(0)) } @@ -145,7 +145,7 @@ class LocalEntityUseCasesTest { ) val entitiesRepository = MeasurableEntitiesRepository(entitiesRepository) - LocalEntityUseCases.updateLocalEntities("songs", csv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", csv, entitiesRepository) assertThat(entitiesRepository.accesses, equalTo(2)) } @@ -154,7 +154,7 @@ class LocalEntityUseCasesTest { entitiesRepository.save(Entity("songs", "noah", "Noah")) val csv = createEntityList(Entity("songs", "cathedrals", "Cathedrals")) - LocalEntityUseCases.updateLocalEntities("songs", csv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", csv, entitiesRepository) val songs = entitiesRepository.getEntities("songs") assertThat( songs, @@ -170,10 +170,10 @@ class LocalEntityUseCasesTest { entitiesRepository.save(Entity("songs", "cathedrals", "Cathedrals")) val firstCsv = createEntityList(Entity("songs", "cathedrals", "Cathedrals")) - LocalEntityUseCases.updateLocalEntities("songs", firstCsv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", firstCsv, entitiesRepository) val secondCsv = createEntityList(Entity("songs", "noah", "Noah")) - LocalEntityUseCases.updateLocalEntities("songs", secondCsv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", secondCsv, entitiesRepository) val songs = entitiesRepository.getEntities("songs") assertThat(songs, containsInAnyOrder(Entity("songs", "noah", "Noah", state = ONLINE))) @@ -185,10 +185,10 @@ class LocalEntityUseCasesTest { val firstCsv = createEntityList(Entity("songs", "cathedrals", "Cathedrals (A Song)", version = 2)) - LocalEntityUseCases.updateLocalEntities("songs", firstCsv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", firstCsv, entitiesRepository) val secondCsv = createEntityList() - LocalEntityUseCases.updateLocalEntities("songs", secondCsv, entitiesRepository) + LocalEntityUseCases.updateLocalEntitiesFromServer("songs", secondCsv, entitiesRepository) val songs = entitiesRepository.getEntities("songs") assertThat(songs.isEmpty(), equalTo(true)) From f0dc34931ee628b9d5bcfbe8519d9e6138441cab Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 15 May 2024 09:46:34 +0100 Subject: [PATCH 513/750] Update test naming --- .../entities/LocalEntityUseCasesTest.kt | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt b/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt index 5143ac44126..c137199e3fb 100644 --- a/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt +++ b/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt @@ -15,7 +15,7 @@ class LocalEntityUseCasesTest { private val entitiesRepository = InMemEntitiesRepository() @Test - fun `updateLocalEntities overrides offline version if the online version is newer`() { + fun `updateLocalEntitiesFromServer overrides offline version if the online version is newer`() { entitiesRepository.save(Entity("songs", "noah", "Noa", 1)) val csv = createEntityList(Entity("songs", "noah", "Noah", 2)) @@ -25,7 +25,7 @@ class LocalEntityUseCasesTest { } @Test - fun `updateLocalEntities does not override offline version if the online version is older`() { + fun `updateLocalEntitiesFromServer does not override offline version if the online version is older`() { entitiesRepository.save(Entity("songs", "noah", "Noah", 2)) val csv = createEntityList(Entity("songs", "noah", "Noa", 1)) @@ -35,7 +35,7 @@ class LocalEntityUseCasesTest { } @Test - fun `updateLocalEntities does not override offline version if the online version is the same`() { + fun `updateLocalEntitiesFromServer does not override offline version if the online version is the same`() { entitiesRepository.save(Entity("songs", "noah", "Noah", 2)) val csv = createEntityList(Entity("songs", "noah", "Noa", 2)) @@ -45,7 +45,7 @@ class LocalEntityUseCasesTest { } @Test - fun `updateLocalEntities ignores properties not in offline version from older online version`() { + fun `updateLocalEntitiesFromServer ignores properties not in offline version from older online version`() { entitiesRepository.save(Entity("songs", "noah", "Noah", 3)) val csv = createEntityList(Entity("songs", "noah", "Noah", 2, listOf(Pair("length", "6:38")))) @@ -59,7 +59,7 @@ class LocalEntityUseCasesTest { } @Test - fun `updateLocalEntities overrides properties in offline version from newer list version`() { + fun `updateLocalEntitiesFromServer overrides properties in offline version from newer list version`() { entitiesRepository.save(Entity("songs", "noah", "Noah", 1, listOf(Pair("length", "6:38")))) val csv = createEntityList(Entity("songs", "noah", "Noah", 2, listOf(Pair("length", "4:58")))) @@ -82,7 +82,7 @@ class LocalEntityUseCasesTest { } @Test - fun `updateLocalEntities does nothing if version does not exist in online entities`() { + fun `updateLocalEntitiesFromServer does nothing if version does not exist in online entities`() { val csv = createCsv( listOf("name", "label"), @@ -94,7 +94,7 @@ class LocalEntityUseCasesTest { } @Test - fun `updateLocalEntities does nothing if name does not exist in online entities`() { + fun `updateLocalEntitiesFromServer does nothing if name does not exist in online entities`() { val csv = createCsv( listOf("label", "__version"), @@ -106,7 +106,7 @@ class LocalEntityUseCasesTest { } @Test - fun `updateLocalEntities does nothing if label does not exist in online entities`() { + fun `updateLocalEntitiesFromServer does nothing if label does not exist in online entities`() { val csv = createCsv( listOf("name", "__version"), @@ -118,7 +118,7 @@ class LocalEntityUseCasesTest { } @Test - fun `updateLocalEntities adds online entity when its label is blank`() { + fun `updateLocalEntitiesFromServer adds online entity when its label is blank`() { val csv = createEntityList(Entity("songs", "cathedrals", label = "")) LocalEntityUseCases.updateLocalEntitiesFromServer("songs", csv, entitiesRepository) @@ -130,7 +130,7 @@ class LocalEntityUseCasesTest { } @Test - fun `updateLocalEntities does nothing if passed a non-CSV file`() { + fun `updateLocalEntitiesFromServer does nothing if passed a non-CSV file`() { val file = TempFiles.createTempFile(".xml") LocalEntityUseCases.updateLocalEntitiesFromServer("songs", file, entitiesRepository) @@ -138,7 +138,7 @@ class LocalEntityUseCasesTest { } @Test - fun `updateLocalEntities accesses entities repo only twice when saving multiple entities`() { + fun `updateLocalEntitiesFromServer accesses entities repo only twice when saving multiple entities`() { val csv = createEntityList( Entity("songs", "noah", "Noah"), Entity("songs", "seven-trumpets", "Seven Trumpets") @@ -150,7 +150,7 @@ class LocalEntityUseCasesTest { } @Test - fun `updateLocalEntities does not remove offline entities that are not in online entities`() { + fun `updateLocalEntitiesFromServer does not remove offline entities that are not in online entities`() { entitiesRepository.save(Entity("songs", "noah", "Noah")) val csv = createEntityList(Entity("songs", "cathedrals", "Cathedrals")) @@ -166,7 +166,7 @@ class LocalEntityUseCasesTest { } @Test - fun `updateLocalEntities removes offline entity that was in online list, but isn't any longer`() { + fun `updateLocalEntitiesFromServer removes offline entity that was in online list, but isn't any longer`() { entitiesRepository.save(Entity("songs", "cathedrals", "Cathedrals")) val firstCsv = createEntityList(Entity("songs", "cathedrals", "Cathedrals")) @@ -180,7 +180,7 @@ class LocalEntityUseCasesTest { } @Test - fun `updateLocalEntities removes offline entity that was updated in online list, but isn't any longer`() { + fun `updateLocalEntitiesFromServer removes offline entity that was updated in online list, but isn't any longer`() { entitiesRepository.save(Entity("songs", "cathedrals", "Cathedrals", version = 1)) val firstCsv = From 95cf77f6d48dfceef7305e2e6bdc0d74675697b4 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 15 May 2024 10:04:26 +0100 Subject: [PATCH 514/750] Don't update entities if they don't exist --- .../collect/entities/LocalEntityUseCases.kt | 25 ++++++++++++++----- .../entities/LocalEntityUseCasesTest.kt | 13 ++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt index 7e667b5786e..647728688c5 100644 --- a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt +++ b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt @@ -2,18 +2,31 @@ package org.odk.collect.entities import org.javarosa.core.model.instance.CsvExternalInstance import org.javarosa.core.model.instance.TreeElement +import org.javarosa.entities.EntityAction import org.javarosa.entities.internal.Entities import java.io.File object LocalEntityUseCases { @JvmStatic - fun updateLocalEntitiesFromForm(entities: Entities?, entitiesRepository: EntitiesRepository) { - entities?.entities?.forEach { - val id = it.id - if (id != null && entitiesRepository.getLists().contains(it.dataset)) { - val entity = Entity(it.dataset, id, it.label, it.version, it.properties) - entitiesRepository.save(entity) + fun updateLocalEntitiesFromForm( + formEntities: Entities?, + entitiesRepository: EntitiesRepository + ) { + formEntities?.entities?.forEach { formEntity -> + val id = formEntity.id + if (id != null && entitiesRepository.getLists().contains(formEntity.dataset)) { + if (formEntity.action != EntityAction.UPDATE || entitiesRepository.getEntities(formEntity.dataset).any { it.id == id }) { + val entity = Entity( + formEntity.dataset, + id, + formEntity.label, + formEntity.version, + formEntity.properties + ) + + entitiesRepository.save(entity) + } } } } diff --git a/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt b/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt index c137199e3fb..838fe19695d 100644 --- a/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt +++ b/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt @@ -5,6 +5,8 @@ import org.apache.commons.csv.CSVPrinter import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.containsInAnyOrder import org.hamcrest.Matchers.equalTo +import org.javarosa.entities.EntityAction +import org.javarosa.entities.internal.Entities import org.junit.Test import org.odk.collect.entities.Entity.State.ONLINE import org.odk.collect.shared.TempFiles @@ -14,6 +16,17 @@ class LocalEntityUseCasesTest { private val entitiesRepository = InMemEntitiesRepository() + @Test + fun `updateLocalEntitiesFromForm does not save updated entity that doesn't already exist`() { + val entity = + org.javarosa.entities.Entity(EntityAction.UPDATE, "things", "1", "1", 1, emptyList()) + val formEntities = Entities(listOf(entity)) + entitiesRepository.addList("things") + + LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository) + assertThat(entitiesRepository.getEntities("things").size, equalTo(0)) + } + @Test fun `updateLocalEntitiesFromServer overrides offline version if the online version is newer`() { entitiesRepository.save(Entity("songs", "noah", "Noa", 1)) From 4386f505986963ce77ddac0f1293484bbc90ac2e Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 27 May 2024 12:44:37 +0100 Subject: [PATCH 515/750] Move to coverage to unit test --- .../feature/formentry/EntityFormTest.kt | 19 ------------------- .../entities/LocalEntityUseCasesTest.kt | 11 +++++++++++ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/EntityFormTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/EntityFormTest.kt index 680557fd671..1a6dd91bfec 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/EntityFormTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/EntityFormTest.kt @@ -108,25 +108,6 @@ class EntityFormTest { .assertTextDoesNotExist("Roman Roy") } - @Test - fun fillingEntityUpdateForm_withoutSelectingEntity_doesNothing() { - testDependencies.server.addForm( - "one-question-entity-update.xml", - listOf(EntityListItem("people.csv")) - ) - - rule.withMatchExactlyProject(testDependencies.server.url) - .setupEntities("people") - - .startBlankForm("One Question Entity Update") - .swipeToNextQuestion("Name") - .swipeToEndScreen() - .clickFinalize() - - .startBlankForm("One Question Entity Update") - .assertText("Roman Roy") - } - @Test fun entityListSecondaryInstancesAreConsistentBetweenFollowUpForms() { testDependencies.server.apply { diff --git a/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt b/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt index 838fe19695d..3c1171e67ff 100644 --- a/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt +++ b/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt @@ -27,6 +27,17 @@ class LocalEntityUseCasesTest { assertThat(entitiesRepository.getEntities("things").size, equalTo(0)) } + @Test + fun `updateLocalEntitiesFromForm does not save entity that doesn't have an ID`() { + val entity = + org.javarosa.entities.Entity(EntityAction.CREATE, "things", null, "1", 1, emptyList()) + val formEntities = Entities(listOf(entity)) + entitiesRepository.addList("things") + + LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository) + assertThat(entitiesRepository.getEntities("things").size, equalTo(0)) + } + @Test fun `updateLocalEntitiesFromServer overrides offline version if the online version is newer`() { entitiesRepository.save(Entity("songs", "noah", "Noa", 1)) From d9dda5111a32e2f0992eabb8a43d9ba829c0b3a9 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 20 May 2024 23:15:12 +0200 Subject: [PATCH 516/750] Added a failing test --- .../feature/formmanagement/FormUpdateTest.kt | 29 +++++++++++++++++++ .../resources/media/external_data_broken.csv | 4 +++ 2 files changed, 33 insertions(+) create mode 100644 test-forms/src/main/resources/media/external_data_broken.csv diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/FormUpdateTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/FormUpdateTest.kt index b1290a3d789..97f39931ec8 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/FormUpdateTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/FormUpdateTest.kt @@ -5,6 +5,7 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain import org.junit.runner.RunWith +import org.odk.collect.android.support.StubOpenRosaServer import org.odk.collect.android.support.TestDependencies import org.odk.collect.android.support.pages.FormEntryPage import org.odk.collect.android.support.pages.MainMenuPage @@ -149,4 +150,32 @@ class FormUpdateTest { .startBlankForm("One Question Last Saved") .assertText("32") } + + @Test + fun itIsPossibleToDownloadAnUpdate_afterDownloadingAndOpeningAFormWithBrokenExternalDataset() { + testDependencies.server.addForm( + "external_select_csv.xml", + listOf(StubOpenRosaServer.MediaFileItem("external_data.csv", "external_data_broken.csv")) + ) + + val mainMenuPage = rule.withProject(testDependencies.server.url) + .clickGetBlankForm() + .clickGetSelected() + .clickOKOnDialog(MainMenuPage()) + .startBlankFormWithError("external select") + .assertText(org.odk.collect.strings.R.string.error_occured) + .clickOKOnDialog(MainMenuPage()) + + testDependencies.server.addForm( + "external_select_csv.xml", + listOf(StubOpenRosaServer.MediaFileItem("external_data.csv")) + ) + + mainMenuPage.clickGetBlankForm() + .clickGetSelected() + .clickOKOnDialog(MainMenuPage()) + .startBlankForm("external select") + .assertTextDoesNotExist("Error Occurred") + .assertTexts("One", "Two", "Three") + } } diff --git a/test-forms/src/main/resources/media/external_data_broken.csv b/test-forms/src/main/resources/media/external_data_broken.csv new file mode 100644 index 00000000000..9548d45efdc --- /dev/null +++ b/test-forms/src/main/resources/media/external_data_broken.csv @@ -0,0 +1,4 @@ +name,label +one,"One +two,Two +three,Three From c94ddee69fdf75824ca4756d7a58bcbf65a1119f Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 20 May 2024 23:15:56 +0200 Subject: [PATCH 517/750] Reset ReferenceManager when closing a form --- .../org/odk/collect/android/activities/FormFillingActivity.java | 2 ++ 1 file changed, 2 insertions(+) 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..969642aedd6 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 @@ -74,6 +74,7 @@ import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.data.helper.Selection; import org.javarosa.core.model.instance.TreeElement; +import org.javarosa.core.reference.ReferenceManager; import org.javarosa.form.api.FormEntryCaption; import org.javarosa.form.api.FormEntryController; import org.javarosa.form.api.FormEntryPrompt; @@ -2109,6 +2110,7 @@ private void finishAndReturnInstance() { } private void exit() { + ReferenceManager.instance().reset(); backgroundLocationViewModel.activityHidden(); formEntryViewModel.exit(); finish(); From 3e985316702d8ae9da009c578f629c15fbbe50d8 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 29 May 2024 11:53:14 +0200 Subject: [PATCH 518/750] Link the issue --- .../collect/android/feature/formmanagement/FormUpdateTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/FormUpdateTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/FormUpdateTest.kt index 97f39931ec8..27e00ca412e 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/FormUpdateTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/FormUpdateTest.kt @@ -151,7 +151,7 @@ class FormUpdateTest { .assertText("32") } - @Test + @Test // https://github.com/getodk/collect/issues/6097 fun itIsPossibleToDownloadAnUpdate_afterDownloadingAndOpeningAFormWithBrokenExternalDataset() { testDependencies.server.addForm( "external_select_csv.xml", From 74911d1efe30c203592e17fc12625c5de75a91a5 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 29 May 2024 11:55:59 +0200 Subject: [PATCH 519/750] Moved resettig reference manager to FormEntryViewModel --- .../org/odk/collect/android/activities/FormFillingActivity.java | 2 -- .../org/odk/collect/android/formentry/FormEntryViewModel.java | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) 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 969642aedd6..93f0c63cc96 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 @@ -74,7 +74,6 @@ import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.data.helper.Selection; import org.javarosa.core.model.instance.TreeElement; -import org.javarosa.core.reference.ReferenceManager; import org.javarosa.form.api.FormEntryCaption; import org.javarosa.form.api.FormEntryController; import org.javarosa.form.api.FormEntryPrompt; @@ -2110,7 +2109,6 @@ private void finishAndReturnInstance() { } private void exit() { - ReferenceManager.instance().reset(); backgroundLocationViewModel.activityHidden(); formEntryViewModel.exit(); finish(); diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryViewModel.java b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryViewModel.java index e53ca3790d9..2c9ef16ea91 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryViewModel.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryViewModel.java @@ -15,6 +15,7 @@ import org.javarosa.core.model.SelectChoice; import org.javarosa.core.model.actions.recordaudio.RecordAudioActionHandler; import org.javarosa.core.model.data.IAnswerData; +import org.javarosa.core.reference.ReferenceManager; import org.javarosa.form.api.FormEntryController; import org.javarosa.form.api.FormEntryPrompt; import org.javarosa.xpath.parser.XPathSyntaxException; @@ -356,6 +357,7 @@ private void updateIndex(boolean isAsync) { public void exit() { formSessionRepository.clear(sessionId); + ReferenceManager.instance().reset(); } public void validate() { From 47b56f8e069ab96520b302482ce13ca5cc5bab96 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 29 May 2024 20:26:11 +0200 Subject: [PATCH 520/750] Make the padding internal to the RadioButton to increase the clickable area --- maps/src/main/res/layout/offline_map_layer.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maps/src/main/res/layout/offline_map_layer.xml b/maps/src/main/res/layout/offline_map_layer.xml index ba41f164492..213b9d59c41 100644 --- a/maps/src/main/res/layout/offline_map_layer.xml +++ b/maps/src/main/res/layout/offline_map_layer.xml @@ -3,8 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:paddingVertical="@dimen/margin_extra_small"> + android:layout_height="wrap_content"> Date: Wed, 29 May 2024 21:13:51 +0200 Subject: [PATCH 521/750] Added new tests to OfflineMapLayersPickerTest and improved the existing ones --- .../maps/layers/OfflineMapLayersPickerTest.kt | 129 ++++++++++++------ .../odk/collect/testshared/EspressoHelpers.kt | 4 + 2 files changed, 90 insertions(+), 43 deletions(-) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index 0598e7a548a..dbc540c046f 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -8,6 +8,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isEnabled import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -28,16 +29,14 @@ import org.odk.collect.settings.InMemSettingsProvider import org.odk.collect.settings.keys.ProjectKeys import org.odk.collect.shared.TempFiles import org.odk.collect.strings.R.string +import org.odk.collect.testshared.EspressoHelpers import org.odk.collect.testshared.FakeScheduler +import org.odk.collect.testshared.RecyclerViewMatcher import org.odk.collect.testshared.RecyclerViewMatcher.Companion.withRecyclerView import org.odk.collect.webpage.ExternalWebPageHelper @RunWith(AndroidJUnit4::class) class OfflineMapLayersPickerTest { - private val layers = listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), - ReferenceLayer("2", TempFiles.createTempFile(), "layer2") - ) private val referenceLayerRepository = mock().also { whenever(it.getAllSupported()).thenReturn(emptyList()) } @@ -57,91 +56,103 @@ class OfflineMapLayersPickerTest { fun `clicking the 'cancel' button dismisses the layers picker`() { val scenario = launchFragment() - scheduler.flush() - scenario.onFragment { assertThat(it.isVisible, equalTo(true)) - onView(withText(string.cancel)).perform(click()) + EspressoHelpers.clickOnText(string.cancel) assertThat(it.isVisible, equalTo(false)) } } @Test - fun `selecting a new layer and clicking the 'cancel' button does not save the layer`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(layers) + fun `clicking the 'cancel' button does not save the layer`() { + whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + )) launchFragment() scheduler.flush() - onView(withText("layer2")).perform(click()) - onView(withText(string.cancel)).perform(click()) + EspressoHelpers.clickOnText(string.cancel) assertThat(settingsProvider.getUnprotectedSettings().contains(ProjectKeys.KEY_REFERENCE_LAYER), equalTo(false)) } + @Test + fun `the 'cancel' button should be enabled during loading layers`() { + launchFragment() + + onView(withText(string.cancel)).check(matches(isEnabled())) + } + @Test fun `clicking the 'save' button dismisses the layers picker`() { val scenario = launchFragment() - scheduler.flush() - scenario.onFragment { assertThat(it.isVisible, equalTo(true)) - onView(withText(string.save)).perform(click()) + EspressoHelpers.clickOnText(string.save) assertThat(it.isVisible, equalTo(false)) } } @Test - fun `clicking the 'save' button saves null when none of the layers is selected`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(layers) + fun `clicking the 'save' button saves null when 'None' option is checked`() { + whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + )) launchFragment() scheduler.flush() - onView(withText(string.save)).perform(click()) + EspressoHelpers.clickOnText(string.save) assertThat(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER), equalTo(null)) } @Test - fun `clicking the 'save' button saves the current layer`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(layers) - settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, "2") + fun `clicking the 'save' button saves the layer id if any is checked`() { + whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + )) launchFragment() scheduler.flush() - onView(withText(string.save)).perform(click()) - assertThat(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER), equalTo("2")) + EspressoHelpers.clickOnText("layer1") + EspressoHelpers.clickOnText(string.save) + assertThat(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER), equalTo("1")) } @Test - fun `selecting a new layer and clicking the 'save' button saves the new layer`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(layers) + fun `when no layer id is saved in settings the 'None' option should be checked`() { + whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + )) launchFragment() scheduler.flush() - onView(withText("layer2")).perform(click()) - onView(withText(string.save)).perform(click()) - assertThat(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER), equalTo("2")) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(isChecked())) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(not(isChecked()))) } @Test - fun `selecting 'none' and clicking the 'save' button saves null`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(layers) + fun `when layer id is saved in settings the layer it belongs to should be checked`() { + whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), + ReferenceLayer("2", TempFiles.createTempFile(), "layer2") + )) settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, "2") launchFragment() scheduler.flush() - onView(withText(string.none)).perform(click()) - onView(withText(string.save)).perform(click()) - assertThat(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER), equalTo(null)) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(not(isChecked()))) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(not(isChecked()))) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.radio_button)).check(matches(isChecked())) } @Test @@ -157,6 +168,13 @@ class OfflineMapLayersPickerTest { onView(withId(R.id.layers)).check(matches(isDisplayed())) } + @Test + fun `the 'learn more' button should be enabled during loading layers`() { + launchFragment() + + onView(withText(string.get_help_with_reference_layers)).check(matches(isEnabled())) + } + @Test fun `clicking the 'learn more' button opens the forum thread`() { launchFragment() @@ -174,35 +192,60 @@ class OfflineMapLayersPickerTest { scheduler.flush() - onView(withText(string.none)).check(matches(isDisplayed())) + onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(1))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(withText(string.none))) + } + + @Test + fun `if there are multiple layers all of them are displayed along with the 'None'`() { + whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), + ReferenceLayer("2", TempFiles.createTempFile(), "layer2") + )) + + launchFragment() + + scheduler.flush() + + onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(withText(string.none))) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(withText("layer1"))) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.radio_button)).check(matches(withText("layer2"))) } @Test - fun `if there are multiple layers all of them are displayed along with the 'none' option`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(layers) + fun `checking layers sets selection correctly`() { + whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + )) launchFragment() scheduler.flush() - onView(withText(string.none)).check(matches(isDisplayed())) - onView(withText("layer1")).check(matches(isDisplayed())) - onView(withText("layer2")).check(matches(isDisplayed())) + EspressoHelpers.clickOnText("layer1") + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(not(isChecked()))) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(isChecked())) + + EspressoHelpers.clickOnText(string.none) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(isChecked())) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(not(isChecked()))) } @Test fun `recreating maintains selection`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(layers) + whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + )) val scenario = launchFragment() scheduler.flush() - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(isChecked())) - onView(withText("layer2")).perform(click()) - onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.radio_button)).check(matches(isChecked())) + EspressoHelpers.clickOnText("layer1") scenario.recreate() - onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.radio_button)).check(matches(isChecked())) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(not(isChecked()))) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(isChecked())) } private fun launchFragment(): FragmentScenario { diff --git a/test-shared/src/main/java/org/odk/collect/testshared/EspressoHelpers.kt b/test-shared/src/main/java/org/odk/collect/testshared/EspressoHelpers.kt index 7cbf1d16c2b..ac45a31d9d1 100644 --- a/test-shared/src/main/java/org/odk/collect/testshared/EspressoHelpers.kt +++ b/test-shared/src/main/java/org/odk/collect/testshared/EspressoHelpers.kt @@ -38,6 +38,10 @@ object EspressoHelpers { onView(withText(string)).perform(click()) } + fun clickOnText(string: String) { + onView(withText(string)).perform(click()) + } + fun clickOnTextInDialog(string: Int) { onView(withText(string)).inRoot(isDialog()).perform(click()) } From d63aa58cde045b43e6a1bf48f24ccaf5d95ce0fc Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 29 May 2024 21:20:18 +0200 Subject: [PATCH 522/750] Disable the save button during loading layers --- .../odk/collect/maps/layers/OfflineMapLayersPicker.kt | 1 + .../src/main/res/layout/offline_map_layers_picker.xml | 1 + .../collect/maps/layers/OfflineMapLayersPickerTest.kt | 11 +++++++++++ 3 files changed, 13 insertions(+) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index f7ca427929b..00a177f1e28 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -42,6 +42,7 @@ class OfflineMapLayersPicker( viewModel.data.observe(this) { data -> offlineMapLayersPickerBinding.progressIndicator.visibility = View.GONE offlineMapLayersPickerBinding.layers.visibility = View.VISIBLE + offlineMapLayersPickerBinding.save.isEnabled = true val offlineMapLayersAdapter = OfflineMapLayersAdapter(data.first, data.second) { viewModel.changeSelectedLayerId(it) diff --git a/maps/src/main/res/layout/offline_map_layers_picker.xml b/maps/src/main/res/layout/offline_map_layers_picker.xml index 316975b785b..ce4a68a1cf4 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker.xml @@ -125,6 +125,7 @@ android:layout_height="wrap_content" android:text="@string/save" android:layout_marginBottom="@dimen/margin_extra_extra_small" + android:enabled="false" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/guideline_end" /> diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index dbc540c046f..631123f887a 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -88,6 +88,8 @@ class OfflineMapLayersPickerTest { fun `clicking the 'save' button dismisses the layers picker`() { val scenario = launchFragment() + scheduler.flush() + scenario.onFragment { assertThat(it.isVisible, equalTo(true)) EspressoHelpers.clickOnText(string.save) @@ -95,6 +97,15 @@ class OfflineMapLayersPickerTest { } } + @Test + fun `the 'save' button should be disabled during loading layers`() { + launchFragment() + + onView(withText(string.save)).check(matches(not(isEnabled()))) + scheduler.flush() + onView(withText(string.save)).check(matches(isEnabled())) + } + @Test fun `clicking the 'save' button saves null when 'None' option is checked`() { whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( From 901a9ab2646dfe4720ad7ce33c7ca7d82fa9ffd0 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 29 May 2024 22:15:59 +0200 Subject: [PATCH 523/750] Read the file name using the existing method provided by MapConfigurator --- .../layers/DirectoryReferenceLayerRepository.kt | 3 +-- .../DirectoryReferenceLayerRepositoryTest.kt | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt index 821d7552334..2cdc8b5cfa9 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt @@ -40,7 +40,6 @@ class DirectoryReferenceLayerRepository( PathUtils.getRelativeFilePath(directoryPath, file.absolutePath) private fun getName(file: File): String { - val name = MbtilesFile.readName(file) - return name ?: file.getName() + return getMapConfigurator().getDisplayName(file) } } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index 2985d6ace86..0dc046c12fb 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -182,4 +182,19 @@ class DirectoryReferenceLayerRepositoryTest { file.delete() assertThat(repository.get(fileLayer.id), equalTo(null)) } + + @Test + fun get_returnsLayerWithCorrectName() { + val dir = TempFiles.createTempDir() + val file = TempFiles.createTempFile(dir) + + val mapConfigurator = mock().also { + whenever(it.getDisplayName(file)).thenReturn("blah") + } + + val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } + val fileLayer = repository.getAll().first { it.file == file } + + assertThat(repository.get(fileLayer.id)!!.name, equalTo("blah")) + } } From a6c52cb211fb1304f0f1d646f93a1c984c67c0f6 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 29 May 2024 22:51:00 +0200 Subject: [PATCH 524/750] #getAll should filter out not supported layers --- .../DirectoryReferenceLayerRepository.kt | 11 +- .../layers/OfflineMapLayersPickerViewModel.kt | 2 +- .../maps/layers/ReferenceLayerRepository.kt | 1 - .../DirectoryReferenceLayerRepositoryTest.kt | 121 ++++++++---------- .../maps/layers/OfflineMapLayersPickerTest.kt | 18 +-- 5 files changed, 67 insertions(+), 86 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt index 2cdc8b5cfa9..4f3158c520f 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt @@ -11,13 +11,10 @@ class DirectoryReferenceLayerRepository( ) : ReferenceLayerRepository { override fun getAll(): List { - return getAllFilesWithDirectory().map { - ReferenceLayer(getIdForFile(it.second, it.first), it.first, getName(it.first)) - }.distinctBy { it.id } - } - - override fun getAllSupported(): List { - return getAll().filter { getMapConfigurator().supportsLayer(it.file) } + return getAllFilesWithDirectory() + .map { ReferenceLayer(getIdForFile(it.second, it.first), it.first, getName(it.first)) } + .distinctBy { it.id } + .filter { getMapConfigurator().supportsLayer(it.file) } } override fun get(id: String): ReferenceLayer? { diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt index ae08dcbd452..a53aa25b82b 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt @@ -18,7 +18,7 @@ class OfflineMapLayersPickerViewModel( init { scheduler.immediate( background = { - val layers = referenceLayerRepository.getAllSupported() + val layers = referenceLayerRepository.getAll() val selectedLayerId = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) _data.postValue(Pair(layers, selectedLayerId)) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt index a08cae6c857..58c17cadea5 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt @@ -5,7 +5,6 @@ import java.io.File interface ReferenceLayerRepository { fun getAll(): List - fun getAllSupported(): List fun get(id: String): ReferenceLayer? } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index 0dc046c12fb..22ee184ceff 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -10,91 +10,47 @@ import org.odk.collect.maps.MapConfigurator import org.odk.collect.shared.TempFiles class DirectoryReferenceLayerRepositoryTest { - @Test - fun getAll_returnsAllLayersInTheDirectory() { - val dir = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir) - val file2 = TempFiles.createTempFile(dir) - - val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath), mock()) - assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file2)) - } - - @Test - fun getAll_returnsAllLayersInSubDirectories() { - val dir1 = TempFiles.createTempDir() - val dir2 = TempFiles.createTempDir(dir1) - val file1 = TempFiles.createTempFile(dir2) - val file2 = TempFiles.createTempFile(dir2) - - val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath), mock()) - assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file2)) - } - - @Test - fun getAll_withMultipleDirectories_returnsAllLayersInAllDirectories() { - val dir1 = TempFiles.createTempDir() - val dir2 = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir1) - val file2 = TempFiles.createTempFile(dir2) - - val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath), mock()) - assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file2)) - } - - /** - * We do this so we don't end up returns a list of reference layers with non-unique IDs. If two - * (or more) files have the same relative path, only the first one (in the order of declared - * layer directories) will be returned. - */ - @Test - fun getAll_withMultipleDirectoriesWithFilesWithTheSameRelativePath_onlyReturnsTheFileFromTheFirstDirectory() { - val dir1 = TempFiles.createTempDir() - val dir2 = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir1, "blah", ".temp") - TempFiles.createTempFile(dir2, "blah", ".temp") - - val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath), mock()) - assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1)) - } - - @Test - fun getAllSupported_returnsAllSupportedLayersInTheDirectory() { + fun getAll_returnsAllSupportedLayersInTheDirectory() { val dir = TempFiles.createTempDir() val file1 = TempFiles.createTempFile(dir) val file2 = TempFiles.createTempFile(dir) val file3 = TempFiles.createTempFile(dir) val mapConfigurator = mock().also { whenever(it.supportsLayer(file1)).thenReturn(true) + whenever(it.getDisplayName(file1)).thenReturn("file1") whenever(it.supportsLayer(file2)).thenReturn(false) + whenever(it.getDisplayName(file2)).thenReturn("file2") whenever(it.supportsLayer(file3)).thenReturn(true) + whenever(it.getDisplayName(file3)).thenReturn("file3") } val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } - assertThat(repository.getAllSupported().map { it.file }, containsInAnyOrder(file1, file3)) + assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3)) } @Test - fun getAllSupported_returnsAllSupportedLayersInSubDirectories() { + fun getAll_returnsAllSupportedLayersInSubDirectories() { val dir1 = TempFiles.createTempDir() val dir2 = TempFiles.createTempDir(dir1) - val dir3 = TempFiles.createTempDir(dir1) val file1 = TempFiles.createTempFile(dir2) val file2 = TempFiles.createTempFile(dir2) - val file3 = TempFiles.createTempFile(dir3) + val file3 = TempFiles.createTempFile(dir2) val mapConfigurator = mock().also { whenever(it.supportsLayer(file1)).thenReturn(true) + whenever(it.getDisplayName(file1)).thenReturn("file1") whenever(it.supportsLayer(file2)).thenReturn(false) + whenever(it.getDisplayName(file2)).thenReturn("file2") whenever(it.supportsLayer(file3)).thenReturn(true) + whenever(it.getDisplayName(file3)).thenReturn("file3") } val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath)) { mapConfigurator } - assertThat(repository.getAllSupported().map { it.file }, containsInAnyOrder(file1, file3)) + assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3)) } @Test - fun getAllSupported_withMultipleDirectories_returnsAllSupportedLayersInAllDirectories() { + fun getAll_withMultipleDirectories_returnsAllSupportedLayersInAllDirectories() { val dir1 = TempFiles.createTempDir() val dir2 = TempFiles.createTempDir() val dir3 = TempFiles.createTempDir() @@ -103,13 +59,16 @@ class DirectoryReferenceLayerRepositoryTest { val file3 = TempFiles.createTempFile(dir3) val mapConfigurator = mock().also { whenever(it.supportsLayer(file1)).thenReturn(true) + whenever(it.getDisplayName(file1)).thenReturn("file1") whenever(it.supportsLayer(file2)).thenReturn(false) + whenever(it.getDisplayName(file2)).thenReturn("file2") whenever(it.supportsLayer(file3)).thenReturn(true) + whenever(it.getDisplayName(file3)).thenReturn("file3") } val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath, dir3.absolutePath)) { mapConfigurator } - assertThat(repository.getAllSupported().map { it.file }, containsInAnyOrder(file1, file3)) + assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3)) } /** @@ -118,7 +77,7 @@ class DirectoryReferenceLayerRepositoryTest { * layer directories) will be returned. */ @Test - fun getAllSupported_withMultipleDirectoriesWithFilesWithTheSameRelativePath_onlyReturnsTheSupportedFileFromTheFirstDirectory() { + fun getAll_withMultipleDirectoriesWithFilesWithTheSameRelativePath_onlyReturnsTheSupportedFileFromTheFirstDirectory() { val dir1 = TempFiles.createTempDir() val dir2 = TempFiles.createTempDir() val dir3 = TempFiles.createTempDir() @@ -127,22 +86,31 @@ class DirectoryReferenceLayerRepositoryTest { val file3 = TempFiles.createTempFile(dir3, "blah", ".temp") val mapConfigurator = mock().also { whenever(it.supportsLayer(file1)).thenReturn(true) + whenever(it.getDisplayName(file1)).thenReturn("file1") whenever(it.supportsLayer(file2)).thenReturn(false) + whenever(it.getDisplayName(file2)).thenReturn("file2") whenever(it.supportsLayer(file3)).thenReturn(true) + whenever(it.getDisplayName(file3)).thenReturn("file3") } val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath, dir3.absolutePath)) { mapConfigurator } - assertThat(repository.getAllSupported().map { it.file }, containsInAnyOrder(file1)) + assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1)) } @Test fun get_returnsLayer() { val dir = TempFiles.createTempDir() - TempFiles.createTempFile(dir) + val file1 = TempFiles.createTempFile(dir) val file2 = TempFiles.createTempFile(dir) + val mapConfigurator = mock().also { + whenever(it.getDisplayName(file1)).thenReturn("file1") + whenever(it.supportsLayer(file1)).thenReturn(true) + whenever(it.getDisplayName(file2)).thenReturn("file2") + whenever(it.supportsLayer(file2)).thenReturn(true) + } - val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath), mock()) + val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } val file2Layer = repository.getAll().first { it.file == file2 } assertThat(repository.get(file2Layer.id)!!.file, equalTo(file2)) } @@ -151,10 +119,16 @@ class DirectoryReferenceLayerRepositoryTest { fun get_withMultipleDirectories_returnsLayer() { val dir1 = TempFiles.createTempDir() val dir2 = TempFiles.createTempDir() - TempFiles.createTempFile(dir1) + val file1 = TempFiles.createTempFile(dir1) val file2 = TempFiles.createTempFile(dir2) + val mapConfigurator = mock().also { + whenever(it.getDisplayName(file1)).thenReturn("file1") + whenever(it.supportsLayer(file1)).thenReturn(true) + whenever(it.getDisplayName(file2)).thenReturn("file2") + whenever(it.supportsLayer(file2)).thenReturn(true) + } - val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath), mock()) + val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath)) { mapConfigurator } val file2Layer = repository.getAll().first { it.file == file2 } assertThat(repository.get(file2Layer.id)!!.file, equalTo(file2)) } @@ -164,9 +138,15 @@ class DirectoryReferenceLayerRepositoryTest { val dir1 = TempFiles.createTempDir() val dir2 = TempFiles.createTempDir() val file1 = TempFiles.createTempFile(dir1, "blah", ".temp") - TempFiles.createTempFile(dir2, "blah", ".temp") + val file2 = TempFiles.createTempFile(dir2, "blah", ".temp") + val mapConfigurator = mock().also { + whenever(it.getDisplayName(file1)).thenReturn("file1") + whenever(it.supportsLayer(file1)).thenReturn(true) + whenever(it.getDisplayName(file2)).thenReturn("file2") + whenever(it.supportsLayer(file2)).thenReturn(true) + } - val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath), mock()) + val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath)) { mapConfigurator } val layerId = repository.getAll().first().id assertThat(repository.get(layerId)!!.file, equalTo(file1)) } @@ -175,8 +155,12 @@ class DirectoryReferenceLayerRepositoryTest { fun get_whenFileDoesNotExist_returnsNull() { val dir = TempFiles.createTempDir() val file = TempFiles.createTempFile(dir) + val mapConfigurator = mock().also { + whenever(it.supportsLayer(file)).thenReturn(true) + whenever(it.getDisplayName(file)).thenReturn("file") + } - val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath), mock()) + val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } val fileLayer = repository.getAll().first { it.file == file } file.delete() @@ -189,12 +173,13 @@ class DirectoryReferenceLayerRepositoryTest { val file = TempFiles.createTempFile(dir) val mapConfigurator = mock().also { - whenever(it.getDisplayName(file)).thenReturn("blah") + whenever(it.supportsLayer(file)).thenReturn(true) + whenever(it.getDisplayName(file)).thenReturn("file") } val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } val fileLayer = repository.getAll().first { it.file == file } - assertThat(repository.get(fileLayer.id)!!.name, equalTo("blah")) + assertThat(repository.get(fileLayer.id)!!.name, equalTo("file")) } } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index 631123f887a..e4e6c551b2e 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -38,7 +38,7 @@ import org.odk.collect.webpage.ExternalWebPageHelper @RunWith(AndroidJUnit4::class) class OfflineMapLayersPickerTest { private val referenceLayerRepository = mock().also { - whenever(it.getAllSupported()).thenReturn(emptyList()) + whenever(it.getAll()).thenReturn(emptyList()) } private val scheduler = FakeScheduler() private val settingsProvider = InMemSettingsProvider() @@ -65,7 +65,7 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'cancel' button does not save the layer`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( ReferenceLayer("1", TempFiles.createTempFile(), "layer1") )) @@ -108,7 +108,7 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'save' button saves null when 'None' option is checked`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( ReferenceLayer("1", TempFiles.createTempFile(), "layer1") )) @@ -122,7 +122,7 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'save' button saves the layer id if any is checked`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( ReferenceLayer("1", TempFiles.createTempFile(), "layer1") )) @@ -137,7 +137,7 @@ class OfflineMapLayersPickerTest { @Test fun `when no layer id is saved in settings the 'None' option should be checked`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( ReferenceLayer("1", TempFiles.createTempFile(), "layer1") )) @@ -151,7 +151,7 @@ class OfflineMapLayersPickerTest { @Test fun `when layer id is saved in settings the layer it belongs to should be checked`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), ReferenceLayer("2", TempFiles.createTempFile(), "layer2") )) @@ -209,7 +209,7 @@ class OfflineMapLayersPickerTest { @Test fun `if there are multiple layers all of them are displayed along with the 'None'`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), ReferenceLayer("2", TempFiles.createTempFile(), "layer2") )) @@ -226,7 +226,7 @@ class OfflineMapLayersPickerTest { @Test fun `checking layers sets selection correctly`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( ReferenceLayer("1", TempFiles.createTempFile(), "layer1") )) @@ -245,7 +245,7 @@ class OfflineMapLayersPickerTest { @Test fun `recreating maintains selection`() { - whenever(referenceLayerRepository.getAllSupported()).thenReturn(listOf( + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( ReferenceLayer("1", TempFiles.createTempFile(), "layer1") )) From 5486a7242ce72cde4955d353a0d8b09019cb4ac7 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 29 May 2024 23:13:35 +0200 Subject: [PATCH 525/750] Fixed MapFragmentReferenceLayerUtilsTest --- .../maps/layers/MapFragmentReferenceLayerUtilsTest.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt index c151bd699ce..70680f7360b 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt @@ -6,6 +6,8 @@ import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.odk.collect.maps.MapConfigurator import org.odk.collect.maps.MapFragment import org.odk.collect.shared.TempFiles.createTempDir import org.robolectric.RobolectricTestRunner @@ -48,10 +50,14 @@ class MapFragmentReferenceLayerUtilsTest { val config = Bundle() config.putString(MapFragment.KEY_REFERENCE_LAYER, "blah") + val mapConfigurator = mock().also { + whenever(it.getDisplayName(File(layersPath, "blah"))).thenReturn("blah") + } + assertNotNull( MapFragmentReferenceLayerUtils.getReferenceLayerFile( config, - DirectoryReferenceLayerRepository(listOf(layersPath), mock()) + DirectoryReferenceLayerRepository(listOf(layersPath)) { mapConfigurator } ) ) } From f0e120f41b4fafe917bfa3f24a5471d51e419e65 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 3 Jun 2024 13:47:42 +0100 Subject: [PATCH 526/750] Update command used to install local JavaRosa --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ad1c16bd1d..e046d26e106 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ JavaRosa is the form engine that powers Collect. If you want to debug or change 1. Build and install your changes of JavaRosa (into your local Maven repo): ```bash -./gradlew installLocal +./gradlew publishToMavenLocal ``` 1. Change `const val javarosa = javarosa_online` in `Dependencies.kt` to `const val javarosa = javarosa_local` From 85a5900c539df9e7b3f7247cb8fc053e3bea8cbd Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 3 Jun 2024 15:08:42 +0200 Subject: [PATCH 527/750] Make the recyclerview full-width --- androidshared/src/main/res/drawable/radio_button_inset.xml | 5 +++++ maps/src/main/res/layout/offline_map_layer.xml | 6 ++---- maps/src/main/res/layout/offline_map_layers_picker.xml | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 androidshared/src/main/res/drawable/radio_button_inset.xml diff --git a/androidshared/src/main/res/drawable/radio_button_inset.xml b/androidshared/src/main/res/drawable/radio_button_inset.xml new file mode 100644 index 00000000000..1b1c88bac87 --- /dev/null +++ b/androidshared/src/main/res/drawable/radio_button_inset.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/maps/src/main/res/layout/offline_map_layer.xml b/maps/src/main/res/layout/offline_map_layer.xml index 213b9d59c41..664415f42f5 100644 --- a/maps/src/main/res/layout/offline_map_layer.xml +++ b/maps/src/main/res/layout/offline_map_layer.xml @@ -9,10 +9,8 @@ android:id="@+id/radio_button" android:layout_width="0dp" android:layout_height="wrap_content" - android:minWidth="0dp" - android:minHeight="0dp" - android:translationX="-5dp" - android:paddingStart="@dimen/margin_standard" + android:button="@drawable/radio_button_inset" + android:paddingEnd="@dimen/margin_standard" android:paddingVertical="@dimen/margin_extra_small" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/maps/src/main/res/layout/offline_map_layers_picker.xml b/maps/src/main/res/layout/offline_map_layers_picker.xml index ce4a68a1cf4..1d8d46d4fca 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker.xml @@ -92,8 +92,8 @@ android:layout_height="0dp" android:visibility="gone" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - app:layout_constraintStart_toEndOf="@id/guideline_start" - app:layout_constraintEnd_toStartOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toTopOf="@id/bottom_divider" app:layout_constraintHeight_default="wrap" app:layout_constraintTop_toBottomOf="@id/top_divider" From 09178a499f48b7f0e6a5d91d54da6193d528907a Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 3 Jun 2024 16:24:27 +0100 Subject: [PATCH 528/750] Remove tracking for editing finalized forms This is no longer possible --- .../activities/InstanceChooserList.java | 22 +------------------ .../android/analytics/AnalyticsEvents.kt | 10 --------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java index a051e5b1225..68b305b5db5 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java @@ -38,27 +38,23 @@ import org.odk.collect.android.R; import org.odk.collect.android.adapters.InstanceListCursorAdapter; -import org.odk.collect.android.analytics.AnalyticsEvents; -import org.odk.collect.android.analytics.AnalyticsUtils; import org.odk.collect.android.dao.CursorLoaderFactory; import org.odk.collect.android.database.instances.DatabaseInstanceColumns; import org.odk.collect.android.entities.EntitiesRepositoryProvider; import org.odk.collect.android.external.FormUriActivity; import org.odk.collect.android.external.InstancesContract; import org.odk.collect.android.formlists.sorting.FormListSortingOption; -import org.odk.collect.android.instancemanagement.InstancesDataService; import org.odk.collect.android.formmanagement.drafts.BulkFinalizationViewModel; import org.odk.collect.android.formmanagement.drafts.DraftsMenuProvider; import org.odk.collect.android.injection.DaggerUtils; import org.odk.collect.android.instancemanagement.FinalizeAllSnackbarPresenter; +import org.odk.collect.android.instancemanagement.InstancesDataService; import org.odk.collect.android.projects.ProjectsDataService; import org.odk.collect.android.utilities.ApplicationConstants; import org.odk.collect.android.utilities.FormsRepositoryProvider; import org.odk.collect.android.utilities.InstancesRepositoryProvider; import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard; import org.odk.collect.async.Scheduler; -import org.odk.collect.forms.Form; -import org.odk.collect.forms.instances.Instance; import org.odk.collect.lists.EmptyListView; import org.odk.collect.material.MaterialProgressDialogFragment; import org.odk.collect.settings.SettingsProvider; @@ -213,7 +209,6 @@ public void onItemClick(AdapterView parent, View view, int position, long id) intent.setData(instanceUri); String formMode = parentIntent.getStringExtra(ApplicationConstants.BundleKeys.FORM_MODE); if (formMode == null || ApplicationConstants.FormModes.EDIT_SAVED.equalsIgnoreCase(formMode)) { - logFormEdit(c); intent.putExtra(ApplicationConstants.BundleKeys.FORM_MODE, ApplicationConstants.FormModes.EDIT_SAVED); formLauncher.launch(intent); } else { @@ -229,21 +224,6 @@ public void onItemClick(AdapterView parent, View view, int position, long id) } } - private void logFormEdit(Cursor cursor) { - String status = cursor.getString(cursor.getColumnIndex(DatabaseInstanceColumns.STATUS)); - String formId = cursor.getString(cursor.getColumnIndex(DatabaseInstanceColumns.JR_FORM_ID)); - String version = cursor.getString(cursor.getColumnIndex(DatabaseInstanceColumns.JR_VERSION)); - - Form form = formsRepositoryProvider.get().getLatestByFormIdAndVersion(formId, version); - String formTitle = form != null ? form.getDisplayName() : ""; - - if (status.equals(Instance.STATUS_INCOMPLETE) || status.equals(Instance.STATUS_INVALID) || status.equals(Instance.STATUS_VALID)) { - AnalyticsUtils.logFormEvent(AnalyticsEvents.EDIT_NON_FINALIZED_FORM, formId, formTitle); - } else if (status.equals(Instance.STATUS_COMPLETE)) { - AnalyticsUtils.logFormEvent(AnalyticsEvents.EDIT_FINALIZED_FORM, formId, formTitle); - } - } - private void setupAdapter() { String[] data = {DatabaseInstanceColumns.DISPLAY_NAME, DatabaseInstanceColumns.DELETED_DATE}; int[] view = {R.id.form_title, R.id.form_subtitle2}; diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index 508881957c3..703eff2ac40 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -124,16 +124,6 @@ object AnalyticsEvents { const val INSTANCE_PROVIDER_DELETE = "InstanceProviderDelete" - /** - * Tracks how often non finalized forms are edited - */ - const val EDIT_NON_FINALIZED_FORM = "EditNonFinalizedForm" - - /** - * Tracks how often finalized forms are edited - */ - const val EDIT_FINALIZED_FORM = "EditFinalizedForm" - /** * Tracks how often form-level auto-delete setting is used */ From 0a53a9927b70f809955cac45660750897334b8ce Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 3 Jun 2024 16:32:53 +0100 Subject: [PATCH 529/750] Deprecate class needing rewritten to remove potential ANRs --- .../odk/collect/android/activities/InstanceChooserList.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java index 68b305b5db5..ba83b0430cf 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java @@ -39,6 +39,7 @@ import org.odk.collect.android.R; import org.odk.collect.android.adapters.InstanceListCursorAdapter; import org.odk.collect.android.dao.CursorLoaderFactory; +import org.odk.collect.android.database.DatabaseConnection; import org.odk.collect.android.database.instances.DatabaseInstanceColumns; import org.odk.collect.android.entities.EntitiesRepositoryProvider; import org.odk.collect.android.external.FormUriActivity; @@ -70,7 +71,10 @@ * * @author Yaw Anokwa (yanokwa@gmail.com) * @author Carl Hartung (carlhartung@gmail.com) + * @deprecated Uses {@link CursorLoaderFactory} and interacts with {@link DatabaseConnection} on the + * UI thread. */ +@Deprecated public class InstanceChooserList extends AppListActivity implements AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks { private static final String INSTANCE_LIST_ACTIVITY_SORTING_ORDER = "instanceListActivitySortingOrder"; private static final String VIEW_SENT_FORM_SORTING_ORDER = "ViewSentFormSortingOrder"; From ab0f4863b864fdf6ae073b591191c7cf48cd665d Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 3 Jun 2024 18:27:59 +0200 Subject: [PATCH 530/750] Improved tests --- .../DirectoryReferenceLayerRepositoryTest.kt | 123 ++++++++++-------- 1 file changed, 71 insertions(+), 52 deletions(-) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index 22ee184ceff..bcd5a6637f1 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -1,13 +1,16 @@ package org.odk.collect.maps.layers +import android.content.Context +import android.os.Bundle +import androidx.preference.Preference import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.containsInAnyOrder import org.hamcrest.Matchers.equalTo import org.junit.Test -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever import org.odk.collect.maps.MapConfigurator import org.odk.collect.shared.TempFiles +import org.odk.collect.shared.settings.Settings +import java.io.File class DirectoryReferenceLayerRepositoryTest { @Test @@ -16,13 +19,10 @@ class DirectoryReferenceLayerRepositoryTest { val file1 = TempFiles.createTempFile(dir) val file2 = TempFiles.createTempFile(dir) val file3 = TempFiles.createTempFile(dir) - val mapConfigurator = mock().also { - whenever(it.supportsLayer(file1)).thenReturn(true) - whenever(it.getDisplayName(file1)).thenReturn("file1") - whenever(it.supportsLayer(file2)).thenReturn(false) - whenever(it.getDisplayName(file2)).thenReturn("file2") - whenever(it.supportsLayer(file3)).thenReturn(true) - whenever(it.getDisplayName(file3)).thenReturn("file3") + val mapConfigurator = StubMapConfigurator().also { + it.addFile(file1, true, file1.name) + it.addFile(file2, false, file2.name) + it.addFile(file3, true, file3.name) } val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } @@ -36,13 +36,10 @@ class DirectoryReferenceLayerRepositoryTest { val file1 = TempFiles.createTempFile(dir2) val file2 = TempFiles.createTempFile(dir2) val file3 = TempFiles.createTempFile(dir2) - val mapConfigurator = mock().also { - whenever(it.supportsLayer(file1)).thenReturn(true) - whenever(it.getDisplayName(file1)).thenReturn("file1") - whenever(it.supportsLayer(file2)).thenReturn(false) - whenever(it.getDisplayName(file2)).thenReturn("file2") - whenever(it.supportsLayer(file3)).thenReturn(true) - whenever(it.getDisplayName(file3)).thenReturn("file3") + val mapConfigurator = StubMapConfigurator().also { + it.addFile(file1, true, file1.name) + it.addFile(file2, false, file2.name) + it.addFile(file3, true, file3.name) } val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath)) { mapConfigurator } @@ -57,13 +54,10 @@ class DirectoryReferenceLayerRepositoryTest { val file1 = TempFiles.createTempFile(dir1) val file2 = TempFiles.createTempFile(dir2) val file3 = TempFiles.createTempFile(dir3) - val mapConfigurator = mock().also { - whenever(it.supportsLayer(file1)).thenReturn(true) - whenever(it.getDisplayName(file1)).thenReturn("file1") - whenever(it.supportsLayer(file2)).thenReturn(false) - whenever(it.getDisplayName(file2)).thenReturn("file2") - whenever(it.supportsLayer(file3)).thenReturn(true) - whenever(it.getDisplayName(file3)).thenReturn("file3") + val mapConfigurator = StubMapConfigurator().also { + it.addFile(file1, true, file1.name) + it.addFile(file2, false, file2.name) + it.addFile(file3, true, file3.name) } val repository = @@ -84,13 +78,10 @@ class DirectoryReferenceLayerRepositoryTest { val file1 = TempFiles.createTempFile(dir1, "blah", ".temp") val file2 = TempFiles.createTempFile(dir2, "blah", ".temp") val file3 = TempFiles.createTempFile(dir3, "blah", ".temp") - val mapConfigurator = mock().also { - whenever(it.supportsLayer(file1)).thenReturn(true) - whenever(it.getDisplayName(file1)).thenReturn("file1") - whenever(it.supportsLayer(file2)).thenReturn(false) - whenever(it.getDisplayName(file2)).thenReturn("file2") - whenever(it.supportsLayer(file3)).thenReturn(true) - whenever(it.getDisplayName(file3)).thenReturn("file3") + val mapConfigurator = StubMapConfigurator().also { + it.addFile(file1, true, file1.name) + it.addFile(file2, false, file2.name) + it.addFile(file3, true, file3.name) } val repository = @@ -103,11 +94,9 @@ class DirectoryReferenceLayerRepositoryTest { val dir = TempFiles.createTempDir() val file1 = TempFiles.createTempFile(dir) val file2 = TempFiles.createTempFile(dir) - val mapConfigurator = mock().also { - whenever(it.getDisplayName(file1)).thenReturn("file1") - whenever(it.supportsLayer(file1)).thenReturn(true) - whenever(it.getDisplayName(file2)).thenReturn("file2") - whenever(it.supportsLayer(file2)).thenReturn(true) + val mapConfigurator = StubMapConfigurator().also { + it.addFile(file1, true, file1.name) + it.addFile(file2, true, file2.name) } val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } @@ -121,11 +110,9 @@ class DirectoryReferenceLayerRepositoryTest { val dir2 = TempFiles.createTempDir() val file1 = TempFiles.createTempFile(dir1) val file2 = TempFiles.createTempFile(dir2) - val mapConfigurator = mock().also { - whenever(it.getDisplayName(file1)).thenReturn("file1") - whenever(it.supportsLayer(file1)).thenReturn(true) - whenever(it.getDisplayName(file2)).thenReturn("file2") - whenever(it.supportsLayer(file2)).thenReturn(true) + val mapConfigurator = StubMapConfigurator().also { + it.addFile(file1, true, file1.name) + it.addFile(file2, true, file2.name) } val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath)) { mapConfigurator } @@ -139,11 +126,9 @@ class DirectoryReferenceLayerRepositoryTest { val dir2 = TempFiles.createTempDir() val file1 = TempFiles.createTempFile(dir1, "blah", ".temp") val file2 = TempFiles.createTempFile(dir2, "blah", ".temp") - val mapConfigurator = mock().also { - whenever(it.getDisplayName(file1)).thenReturn("file1") - whenever(it.supportsLayer(file1)).thenReturn(true) - whenever(it.getDisplayName(file2)).thenReturn("file2") - whenever(it.supportsLayer(file2)).thenReturn(true) + val mapConfigurator = StubMapConfigurator().also { + it.addFile(file1, true, file1.name) + it.addFile(file2, true, file2.name) } val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath)) { mapConfigurator } @@ -155,9 +140,8 @@ class DirectoryReferenceLayerRepositoryTest { fun get_whenFileDoesNotExist_returnsNull() { val dir = TempFiles.createTempDir() val file = TempFiles.createTempFile(dir) - val mapConfigurator = mock().also { - whenever(it.supportsLayer(file)).thenReturn(true) - whenever(it.getDisplayName(file)).thenReturn("file") + val mapConfigurator = StubMapConfigurator().also { + it.addFile(file, true, file.name) } val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } @@ -172,14 +156,49 @@ class DirectoryReferenceLayerRepositoryTest { val dir = TempFiles.createTempDir() val file = TempFiles.createTempFile(dir) - val mapConfigurator = mock().also { - whenever(it.supportsLayer(file)).thenReturn(true) - whenever(it.getDisplayName(file)).thenReturn("file") + val mapConfigurator = StubMapConfigurator().also { + it.addFile(file, true, file.name) } val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } val fileLayer = repository.getAll().first { it.file == file } - assertThat(repository.get(fileLayer.id)!!.name, equalTo("file")) + assertThat(repository.get(fileLayer.id)!!.name, equalTo(file.name)) + } + + private class StubMapConfigurator : MapConfigurator { + private val files = mutableMapOf>() + + override fun supportsLayer(file: File?): Boolean { + return files[file]!!.first + } + + override fun getDisplayName(file: File?): String { + return files[file]!!.second + } + + fun addFile(file: File, isSupported: Boolean, displayName: String) { + files[file] = Pair(isSupported, displayName) + } + + override fun isAvailable(context: Context?): Boolean { + TODO("Not yet implemented") + } + + override fun showUnavailableMessage(context: Context?) { + TODO("Not yet implemented") + } + + override fun createPrefs(context: Context?, settings: Settings?): MutableList { + TODO("Not yet implemented") + } + + override fun getPrefKeys(): MutableCollection { + TODO("Not yet implemented") + } + + override fun buildConfig(prefs: Settings): Bundle { + TODO("Not yet implemented") + } } } From 813b9b72b77444b79558f24b7549a61d029a190b Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 24 May 2024 14:32:26 +0200 Subject: [PATCH 531/750] Added a button for importing layers --- .../maps/layers/OfflineMapLayersPicker.kt | 8 ++++++++ .../res/layout/offline_map_layers_picker.xml | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 00a177f1e28..5efe8b1a2c2 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -32,6 +33,9 @@ class OfflineMapLayersPicker( private lateinit var offlineMapLayersPickerBinding: OfflineMapLayersPickerBinding + private val getLayers = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris -> + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -57,6 +61,10 @@ class OfflineMapLayersPicker( ) } + offlineMapLayersPickerBinding.addLayer.setOnClickListener { + getLayers.launch("*/*") + } + offlineMapLayersPickerBinding.cancel.setOnClickListener { dismiss() } diff --git a/maps/src/main/res/layout/offline_map_layers_picker.xml b/maps/src/main/res/layout/offline_map_layers_picker.xml index 1d8d46d4fca..b6c5d778549 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker.xml @@ -94,7 +94,7 @@ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintBottom_toTopOf="@id/bottom_divider" + app:layout_constraintBottom_toTopOf="@id/add_layer" app:layout_constraintHeight_default="wrap" app:layout_constraintTop_toBottomOf="@id/top_divider" tools:visibility="visible" /> @@ -105,11 +105,23 @@ android:layout_height="wrap_content" android:layout_marginVertical="@dimen/margin_small" android:indeterminate="true" - app:layout_constraintBottom_toTopOf="@id/bottom_divider" + app:layout_constraintBottom_toTopOf="@id/add_layer" app:layout_constraintEnd_toEndOf="@id/message" app:layout_constraintStart_toStartOf="@id/message" app:layout_constraintTop_toBottomOf="@id/top_divider" /> + + Date: Tue, 4 Jun 2024 09:41:17 +0100 Subject: [PATCH 532/750] Fix line numbers in Crashlytics reports --- collect_app/proguard-rules.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/collect_app/proguard-rules.txt b/collect_app/proguard-rules.txt index 4ab6ab0ad6d..19ace210362 100644 --- a/collect_app/proguard-rules.txt +++ b/collect_app/proguard-rules.txt @@ -25,3 +25,6 @@ -keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase -dontwarn org.codehaus.mojo.animal_sniffer.* -dontwarn okhttp3.internal.platform.ConscryptPlatform + +# Keep line numbers for Crashlytics https://stackoverflow.com/questions/38529304/android-crashlytics-sending-incorrect-line-number +-keepattributes SourceFile,LineNumberTable From b37df8ac172fed87974848189b5be8bcba8ffaeb Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Sat, 25 May 2024 00:53:25 +0200 Subject: [PATCH 533/750] Display the confirmation dialog --- .../collect/androidshared/system/UriExt.kt | 38 ++++ .../res/drawable/ic_baseline_layers_24.xml | 13 ++ maps/build.gradle.kts | 1 + .../DirectoryReferenceLayerRepository.kt | 8 + .../layers/OfflineMapLayersImportAdapter.kt | 24 +++ .../layers/OfflineMapLayersImportDialog.kt | 98 ++++++++++ .../OfflineMapLayersImportDialogViewModel.kt | 63 +++++++ .../maps/layers/OfflineMapLayersPicker.kt | 34 ++++ .../layers/OfflineMapLayersPickerViewModel.kt | 24 +-- .../maps/layers/ReferenceLayerRepository.kt | 3 + .../offline_map_layers_import_dialog.xml | 167 ++++++++++++++++++ .../layout/offline_map_layers_import_item.xml | 26 +++ 12 files changed, 489 insertions(+), 10 deletions(-) create mode 100644 androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt create mode 100644 icons/src/main/res/drawable/ic_baseline_layers_24.xml create mode 100644 maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportAdapter.kt create mode 100644 maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialog.kt create mode 100644 maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt create mode 100644 maps/src/main/res/layout/offline_map_layers_import_dialog.xml create mode 100644 maps/src/main/res/layout/offline_map_layers_import_item.xml diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt b/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt new file mode 100644 index 00000000000..99cc1709c57 --- /dev/null +++ b/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt @@ -0,0 +1,38 @@ +package org.odk.collect.androidshared.system + +import android.content.ContentResolver +import android.net.Uri +import android.provider.OpenableColumns +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + +fun Uri.toFile(contentResolver: ContentResolver, dest: File) { + try { + contentResolver.openInputStream(this)?.use { inputStream -> + FileOutputStream(dest).use { outputStream -> + inputStream.copyTo(outputStream) + } + } + } catch (e: IOException) { + Timber.e(e) + } +} + +fun Uri.getFileName(contentResolver: ContentResolver): String? { + var fileName: String? = null + if (scheme == ContentResolver.SCHEME_CONTENT) { + val cursor = contentResolver.query(this, null, null, null, null) + cursor.use { + if (it != null && it.moveToFirst()) { + val fileNameColumnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + fileName = it.getString(fileNameColumnIndex) + } + } + } + if (fileName == null) { + fileName = path?.substringAfterLast("/") + } + return fileName +} diff --git a/icons/src/main/res/drawable/ic_baseline_layers_24.xml b/icons/src/main/res/drawable/ic_baseline_layers_24.xml new file mode 100644 index 00000000000..2a93aa2f714 --- /dev/null +++ b/icons/src/main/res/drawable/ic_baseline_layers_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/maps/build.gradle.kts b/maps/build.gradle.kts index 1807f5b2a85..66c2fd3597b 100644 --- a/maps/build.gradle.kts +++ b/maps/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(project(":shared")) implementation(project(":androidshared")) implementation(project(":icons")) + implementation(project(":material")) implementation(project(":settings")) implementation(project(":strings")) implementation(project(":web-page")) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt index 4f3158c520f..101e0aabb5d 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt @@ -27,6 +27,14 @@ class DirectoryReferenceLayerRepository( } } + override fun getSharedLayersDirPath(): String { + return directoryPaths[1] + } + + override fun getProjectLayersDirPath(): String { + return directoryPaths[0] + } + private fun getAllFilesWithDirectory() = directoryPaths.flatMap { dir -> listFilesRecursively(File(dir)).map { file -> Pair(file, dir) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportAdapter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportAdapter.kt new file mode 100644 index 00000000000..154a7940432 --- /dev/null +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportAdapter.kt @@ -0,0 +1,24 @@ +package org.odk.collect.maps.layers + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.odk.collect.maps.databinding.OfflineMapLayersImportItemBinding + +class OfflineMapLayersImportAdapter( + private val layers: List, +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = OfflineMapLayersImportItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.binding.layerName.text = layers[position].name + } + + override fun getItemCount() = layers.size + + class ViewHolder(val binding: OfflineMapLayersImportItemBinding) : RecyclerView.ViewHolder(binding.root) +} diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialog.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialog.kt new file mode 100644 index 00000000000..da7b38dc638 --- /dev/null +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialog.kt @@ -0,0 +1,98 @@ +package org.odk.collect.maps.layers + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.core.os.bundleOf +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.odk.collect.async.Scheduler +import org.odk.collect.maps.databinding.OfflineMapLayersImportDialogBinding +import org.odk.collect.material.MaterialFullScreenDialogFragment +import org.odk.collect.material.MaterialProgressDialogFragment + +class OfflineMapLayersImportDialog( + private val scheduler: Scheduler, + private val sharedLayersDirPath: String, + private val projectLayersDirPath: String +) : MaterialFullScreenDialogFragment() { + + private val viewModel: OfflineMapLayersImportDialogViewModel by viewModels { + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return OfflineMapLayersImportDialogViewModel(scheduler, requireContext().contentResolver) as T + } + } + } + + private lateinit var binding: OfflineMapLayersImportDialogBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = OfflineMapLayersImportDialogBinding.inflate(inflater) + + viewModel.data.observe(this) { data -> + binding.progressIndicator.visibility = View.GONE + binding.layersList.visibility = View.VISIBLE + binding.addLayerButton.isEnabled = true + + val adapter = OfflineMapLayersImportAdapter(data) + binding.layersList.setAdapter(adapter) + } + viewModel.init(requireArguments().getStringArrayList(URIS)) + + binding.cancelButton.setOnClickListener { + dismiss() + } + + binding.addLayerButton.setOnClickListener { + val layersDir = if (binding.allProjectsOption.isChecked) { + sharedLayersDirPath + } else { + projectLayersDirPath + } + + val isLoading = viewModel.addLayers(layersDir) + MaterialProgressDialogFragment.showOn( + this, + isLoading, + childFragmentManager + ) { + MaterialProgressDialogFragment().also { dialog -> + dialog.message = getString(org.odk.collect.strings.R.string.loading) + } + } + + isLoading.observe(this) { + if (!it) { + setFragmentResult(RESULT_KEY, bundleOf()) + dismiss() + } + } + } + return binding.root + } + + override fun onCloseClicked() { + } + + override fun onBackPressed() { + dismiss() + } + + override fun getToolbar(): Toolbar { + return binding.toolbar + } + + companion object { + const val URIS = "uris" + const val RESULT_KEY = "layersAdded" + } +} diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt new file mode 100644 index 00000000000..7fe15d9d5b3 --- /dev/null +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt @@ -0,0 +1,63 @@ +package org.odk.collect.maps.layers + +import android.content.ContentResolver +import android.net.Uri +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.odk.collect.androidshared.system.getFileName +import org.odk.collect.androidshared.system.toFile +import org.odk.collect.async.Scheduler +import org.odk.collect.shared.TempFiles +import java.io.File +import java.util.ArrayList + +class OfflineMapLayersImportDialogViewModel( + private val scheduler: Scheduler, + private val contentResolver: ContentResolver +) : ViewModel() { + + private val _data = MutableLiveData>() + val data: LiveData> = _data + + private lateinit var tempLayersDir: File + + fun init(uris: ArrayList?) { + scheduler.immediate( + background = { + tempLayersDir = TempFiles.createTempDir().also { + it.deleteOnExit() + } + val layers = mutableListOf() + uris?.forEach { uriString -> + val uri = Uri.parse(uriString) + uri.getFileName(contentResolver)?.let { fileName -> + val layerFile = File(tempLayersDir, fileName).also { file -> + uri.toFile(contentResolver, file) + } + layers.add(ReferenceLayer(layerFile.absolutePath, layerFile, MbtilesFile.readName(layerFile) ?: layerFile.name)) + } + } + _data.postValue(layers) + }, + foreground = { } + ) + } + + fun addLayers(layersDir: String): LiveData { + val isLoading = MutableLiveData(true) + scheduler.immediate( + background = { + val destDir = File(layersDir) + tempLayersDir.listFiles()?.forEach { + it.copyTo(File(destDir, it.name), true) + } + tempLayersDir.delete() + + isLoading.postValue(false) + }, + foreground = { } + ) + return isLoading + } +} diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 5efe8b1a2c2..22145feec5c 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -6,11 +6,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.odk.collect.androidshared.ui.DialogFragmentUtils +import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.androidshared.ui.GroupClickListener.addOnClickListener import org.odk.collect.async.Scheduler import org.odk.collect.maps.databinding.OfflineMapLayersPickerBinding @@ -34,6 +37,36 @@ class OfflineMapLayersPicker( private lateinit var offlineMapLayersPickerBinding: OfflineMapLayersPickerBinding private val getLayers = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris -> + if (uris.isNotEmpty()) { + val uriStrings: MutableList = ArrayList() + for (uri in uris) { + uriStrings.add(uri.toString()) + } + + DialogFragmentUtils.showIfNotShowing( + OfflineMapLayersImportDialog::class.java, + Bundle().apply { putStringArrayList(OfflineMapLayersImportDialog.URIS, ArrayList(uriStrings)) }, + childFragmentManager + ) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + childFragmentManager.fragmentFactory = FragmentFactoryBuilder() + .forClass(OfflineMapLayersImportDialog::class) { + OfflineMapLayersImportDialog( + scheduler, + referenceLayerRepository.getSharedLayersDirPath(), + referenceLayerRepository.getProjectLayersDirPath() + ) + } + .build() + + super.onCreate(savedInstanceState) + + childFragmentManager.setFragmentResultListener(OfflineMapLayersImportDialog.RESULT_KEY, this) { _, _ -> + viewModel.refreshLayers() + } } override fun onCreateView( @@ -73,6 +106,7 @@ class OfflineMapLayersPicker( viewModel.saveSelectedLayer() dismiss() } + return offlineMapLayersPickerBinding.root } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt index a53aa25b82b..95bc2ab1416 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt @@ -9,22 +9,14 @@ import org.odk.collect.settings.keys.ProjectKeys class OfflineMapLayersPickerViewModel( private val referenceLayerRepository: ReferenceLayerRepository, - scheduler: Scheduler, + private val scheduler: Scheduler, private val settingsProvider: SettingsProvider ) : ViewModel() { private val _data = MutableLiveData, String?>>() val data: LiveData, String?>> = _data init { - scheduler.immediate( - background = { - val layers = referenceLayerRepository.getAll() - val selectedLayerId = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) - - _data.postValue(Pair(layers, selectedLayerId)) - }, - foreground = { } - ) + refreshLayers() } fun saveSelectedLayer() { @@ -35,4 +27,16 @@ class OfflineMapLayersPickerViewModel( fun changeSelectedLayerId(selectedLayerId: String?) { _data.postValue(_data.value?.copy(second = selectedLayerId)) } + + fun refreshLayers() { + scheduler.immediate( + background = { + val layers = referenceLayerRepository.getAll() + val selectedLayerId = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) + + _data.postValue(Pair(layers, selectedLayerId)) + }, + foreground = { } + ) + } } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt index 58c17cadea5..fa981fd7cc2 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt @@ -6,6 +6,9 @@ interface ReferenceLayerRepository { fun getAll(): List fun get(id: String): ReferenceLayer? + + fun getSharedLayersDirPath(): String + fun getProjectLayersDirPath(): String } data class ReferenceLayer(val id: String, val file: File, val name: String) diff --git a/maps/src/main/res/layout/offline_map_layers_import_dialog.xml b/maps/src/main/res/layout/offline_map_layers_import_dialog.xml new file mode 100644 index 00000000000..537baf20bbf --- /dev/null +++ b/maps/src/main/res/layout/offline_map_layers_import_dialog.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/maps/src/main/res/layout/offline_map_layers_import_item.xml b/maps/src/main/res/layout/offline_map_layers_import_item.xml new file mode 100644 index 00000000000..c564a94f6a4 --- /dev/null +++ b/maps/src/main/res/layout/offline_map_layers_import_item.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file From 33426f806d2a1cf69839d5fca4629329d83c5be0 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Sat, 25 May 2024 14:08:29 +0200 Subject: [PATCH 534/750] Remove unused code --- .../res/layout/reference_layer_help_footer.xml | 18 ------------------ .../maps/layers/OfflineMapLayersPicker.kt | 4 ---- 2 files changed, 22 deletions(-) delete mode 100644 collect_app/src/main/res/layout/reference_layer_help_footer.xml diff --git a/collect_app/src/main/res/layout/reference_layer_help_footer.xml b/collect_app/src/main/res/layout/reference_layer_help_footer.xml deleted file mode 100644 index 83d0731ca8a..00000000000 --- a/collect_app/src/main/res/layout/reference_layer_help_footer.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 22145feec5c..796562aeeeb 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -120,8 +120,4 @@ class OfflineMapLayersPicker( // ignore } } - - companion object { - const val TAG = "OfflineMapLayersPicker" - } } From d4882e9d2722c8ee663499466c479ee3ae831655 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 4 Jun 2024 11:20:18 +0200 Subject: [PATCH 535/750] Improved DirectoryReferenceLayerRepository to make it clear which layers dir is used --- .../injection/config/AppDependencyModule.java | 4 +-- .../DirectoryReferenceLayerRepository.kt | 9 +++--- .../DirectoryReferenceLayerRepositoryTest.kt | 30 +++++++++---------- .../MapFragmentReferenceLayerUtilsTest.kt | 6 ++-- 4 files changed, 24 insertions(+), 25 deletions(-) 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 984c007bb61..c1acd235054 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 @@ -137,7 +137,6 @@ import org.odk.collect.webpage.ExternalWebPageHelper; import java.io.File; -import java.util.Arrays; import javax.inject.Named; import javax.inject.Singleton; @@ -573,7 +572,8 @@ public PreferenceVisibilityHandler providesDisabledPreferencesRemover(SettingsPr @Provides public ReferenceLayerRepository providesReferenceLayerRepository(StoragePathProvider storagePathProvider, SettingsProvider settingsProvider) { return new DirectoryReferenceLayerRepository( - Arrays.asList(storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS), storagePathProvider.getOdkDirPath(StorageSubdirectory.SHARED_LAYERS)), + storagePathProvider.getOdkDirPath(StorageSubdirectory.SHARED_LAYERS), + storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS), () -> MapConfiguratorProvider.getConfigurator( settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_BASEMAP_SOURCE) ) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt index 101e0aabb5d..627f635882d 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt @@ -6,7 +6,8 @@ import org.odk.collect.shared.files.DirectoryUtils.listFilesRecursively import java.io.File class DirectoryReferenceLayerRepository( - private val directoryPaths: List, + private val sharedLayersDirPath: String, + private val projectLayersDirPath: String, private val getMapConfigurator: () -> MapConfigurator ) : ReferenceLayerRepository { @@ -28,14 +29,14 @@ class DirectoryReferenceLayerRepository( } override fun getSharedLayersDirPath(): String { - return directoryPaths[1] + return sharedLayersDirPath } override fun getProjectLayersDirPath(): String { - return directoryPaths[0] + return projectLayersDirPath } - private fun getAllFilesWithDirectory() = directoryPaths.flatMap { dir -> + private fun getAllFilesWithDirectory() = listOf(sharedLayersDirPath, projectLayersDirPath).flatMap { dir -> listFilesRecursively(File(dir)).map { file -> Pair(file, dir) } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index bcd5a6637f1..b75a929db45 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -25,7 +25,7 @@ class DirectoryReferenceLayerRepositoryTest { it.addFile(file3, true, file3.name) } - val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } + val repository = DirectoryReferenceLayerRepository(dir.absolutePath, "") { mapConfigurator } assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3)) } @@ -42,7 +42,7 @@ class DirectoryReferenceLayerRepositoryTest { it.addFile(file3, true, file3.name) } - val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath)) { mapConfigurator } + val repository = DirectoryReferenceLayerRepository(dir1.absolutePath, "") { mapConfigurator } assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3)) } @@ -50,18 +50,19 @@ class DirectoryReferenceLayerRepositoryTest { fun getAll_withMultipleDirectories_returnsAllSupportedLayersInAllDirectories() { val dir1 = TempFiles.createTempDir() val dir2 = TempFiles.createTempDir() - val dir3 = TempFiles.createTempDir() val file1 = TempFiles.createTempFile(dir1) - val file2 = TempFiles.createTempFile(dir2) - val file3 = TempFiles.createTempFile(dir3) + val file2 = TempFiles.createTempFile(dir1) + val file3 = TempFiles.createTempFile(dir2) + val file4 = TempFiles.createTempFile(dir2) val mapConfigurator = StubMapConfigurator().also { it.addFile(file1, true, file1.name) it.addFile(file2, false, file2.name) it.addFile(file3, true, file3.name) + it.addFile(file4, false, file4.name) } val repository = - DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath, dir3.absolutePath)) { mapConfigurator } + DirectoryReferenceLayerRepository(dir1.absolutePath, dir2.absolutePath) { mapConfigurator } assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3)) } @@ -74,18 +75,15 @@ class DirectoryReferenceLayerRepositoryTest { fun getAll_withMultipleDirectoriesWithFilesWithTheSameRelativePath_onlyReturnsTheSupportedFileFromTheFirstDirectory() { val dir1 = TempFiles.createTempDir() val dir2 = TempFiles.createTempDir() - val dir3 = TempFiles.createTempDir() val file1 = TempFiles.createTempFile(dir1, "blah", ".temp") val file2 = TempFiles.createTempFile(dir2, "blah", ".temp") - val file3 = TempFiles.createTempFile(dir3, "blah", ".temp") val mapConfigurator = StubMapConfigurator().also { it.addFile(file1, true, file1.name) - it.addFile(file2, false, file2.name) - it.addFile(file3, true, file3.name) + it.addFile(file2, true, file2.name) } val repository = - DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath, dir3.absolutePath)) { mapConfigurator } + DirectoryReferenceLayerRepository(dir1.absolutePath, dir2.absolutePath) { mapConfigurator } assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1)) } @@ -99,7 +97,7 @@ class DirectoryReferenceLayerRepositoryTest { it.addFile(file2, true, file2.name) } - val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } + val repository = DirectoryReferenceLayerRepository(dir.absolutePath, "") { mapConfigurator } val file2Layer = repository.getAll().first { it.file == file2 } assertThat(repository.get(file2Layer.id)!!.file, equalTo(file2)) } @@ -115,7 +113,7 @@ class DirectoryReferenceLayerRepositoryTest { it.addFile(file2, true, file2.name) } - val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath)) { mapConfigurator } + val repository = DirectoryReferenceLayerRepository(dir1.absolutePath, dir2.absolutePath) { mapConfigurator } val file2Layer = repository.getAll().first { it.file == file2 } assertThat(repository.get(file2Layer.id)!!.file, equalTo(file2)) } @@ -131,7 +129,7 @@ class DirectoryReferenceLayerRepositoryTest { it.addFile(file2, true, file2.name) } - val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath)) { mapConfigurator } + val repository = DirectoryReferenceLayerRepository(dir1.absolutePath, dir2.absolutePath) { mapConfigurator } val layerId = repository.getAll().first().id assertThat(repository.get(layerId)!!.file, equalTo(file1)) } @@ -144,7 +142,7 @@ class DirectoryReferenceLayerRepositoryTest { it.addFile(file, true, file.name) } - val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } + val repository = DirectoryReferenceLayerRepository(dir.absolutePath, "") { mapConfigurator } val fileLayer = repository.getAll().first { it.file == file } file.delete() @@ -160,7 +158,7 @@ class DirectoryReferenceLayerRepositoryTest { it.addFile(file, true, file.name) } - val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } + val repository = DirectoryReferenceLayerRepository(dir.absolutePath, "") { mapConfigurator } val fileLayer = repository.getAll().first { it.file == file } assertThat(repository.get(fileLayer.id)!!.name, equalTo(file.name)) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt index 70680f7360b..36b91bd7f7a 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt @@ -24,7 +24,7 @@ class MapFragmentReferenceLayerUtilsTest { assertNull( MapFragmentReferenceLayerUtils.getReferenceLayerFile( config, - DirectoryReferenceLayerRepository(listOf(layersPath), mock()) + DirectoryReferenceLayerRepository(layersPath, "", mock()) ) ) } @@ -37,7 +37,7 @@ class MapFragmentReferenceLayerUtilsTest { assertNull( MapFragmentReferenceLayerUtils.getReferenceLayerFile( config, - DirectoryReferenceLayerRepository(listOf(layersPath), mock()) + DirectoryReferenceLayerRepository(layersPath, "", mock()) ) ) } @@ -57,7 +57,7 @@ class MapFragmentReferenceLayerUtilsTest { assertNotNull( MapFragmentReferenceLayerUtils.getReferenceLayerFile( config, - DirectoryReferenceLayerRepository(listOf(layersPath)) { mapConfigurator } + DirectoryReferenceLayerRepository(layersPath, "") { mapConfigurator } ) ) } From 705b1b9feac0c8e86db4191430b6d28b9c9c1f0a Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Sat, 25 May 2024 15:39:39 +0200 Subject: [PATCH 536/750] Converted MapConfigurator to kotlin --- .../screens/MapsPreferencesFragment.kt | 6 +-- ...apConfigurator.java => MapConfigurator.kt} | 43 ++++++++----------- .../DirectoryReferenceLayerRepositoryTest.kt | 15 +++---- 3 files changed, 29 insertions(+), 35 deletions(-) rename maps/src/main/java/org/odk/collect/maps/{MapConfigurator.java => MapConfigurator.kt} (67%) diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt index a20dc55e805..9bfa847b244 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt @@ -110,8 +110,8 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP onBasemapSourceChanged(MapConfiguratorProvider.getConfigurator()) basemapSourcePref.setOnPreferenceChangeListener { _: Preference?, value: Any -> val cftor = MapConfiguratorProvider.getConfigurator(value.toString()) - if (!cftor.isAvailable(context)) { - cftor.showUnavailableMessage(context) + if (!cftor.isAvailable(requireContext())) { + cftor.showUnavailableMessage(requireContext()) false } else { onBasemapSourceChanged(cftor) @@ -142,7 +142,7 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP val baseCategory = findPreference(CATEGORY_BASEMAP) baseCategory!!.removeAll() baseCategory.addPreference(basemapSourcePref) - for (pref in cftor.createPrefs(context, settingsProvider.getUnprotectedSettings())) { + for (pref in cftor.createPrefs(requireContext(), settingsProvider.getUnprotectedSettings())) { pref.isIconSpaceReserved = false baseCategory.addPreference(pref) } diff --git a/maps/src/main/java/org/odk/collect/maps/MapConfigurator.java b/maps/src/main/java/org/odk/collect/maps/MapConfigurator.kt similarity index 67% rename from maps/src/main/java/org/odk/collect/maps/MapConfigurator.java rename to maps/src/main/java/org/odk/collect/maps/MapConfigurator.kt index 4f7da5e86bf..6c8af62df40 100644 --- a/maps/src/main/java/org/odk/collect/maps/MapConfigurator.java +++ b/maps/src/main/java/org/odk/collect/maps/MapConfigurator.kt @@ -1,15 +1,10 @@ -package org.odk.collect.maps; +package org.odk.collect.maps -import android.content.Context; -import android.os.Bundle; - -import androidx.preference.Preference; - -import org.odk.collect.shared.settings.Settings; - -import java.io.File; -import java.util.Collection; -import java.util.List; +import android.content.Context +import android.os.Bundle +import androidx.preference.Preference +import org.odk.collect.shared.settings.Settings +import java.io.File /** * For each MapFragment implementation class, there is one instance of this @@ -22,33 +17,33 @@ * For example, the GoogleMapConfigurator can define a "Google map style" * preference with choices such as Terrain or Satellite. */ -public interface MapConfigurator { - /** Returns true if this MapFragment implementation is available on this device. */ - boolean isAvailable(Context context); +interface MapConfigurator { + /** Returns true if this MapFragment implementation is available on this device. */ + fun isAvailable(context: Context): Boolean /** * Displays a warning to the user that this MapFragment implementation is * unavailable. This will be invoked when isSupported() is false or * createMapFragment(context) returns null. */ - void showUnavailableMessage(Context context); + fun showUnavailableMessage(context: Context) - /** Constructs any preference widgets that are specific to this map implementation. */ - List createPrefs(Context context, Settings settings); + /** Constructs any preference widgets that are specific to this map implementation. */ + fun createPrefs(context: Context, settings: Settings): List - /** Gets the set of keys for preferences that should be watched for changes. */ - Collection getPrefKeys(); + /** Gets the set of keys for preferences that should be watched for changes. */ + val prefKeys: Collection - /** Packs map-related preferences into a Bundle for MapFragment.applyConfig(). */ - Bundle buildConfig(Settings prefs); + /** Packs map-related preferences into a Bundle for MapFragment.applyConfig(). */ + fun buildConfig(prefs: Settings): Bundle /** * Returns true if map fragments obtained from this MapConfigurator are * expected to be able to render the given file as an overlay. This * check determines which files appear as available Reference Layers. */ - boolean supportsLayer(File file); + fun supportsLayer(file: File): Boolean - /** Returns a String name for a given overlay file, or null if unsupported. */ - String getDisplayName(File file); + /** Returns a String name for a given overlay file, or null if unsupported. */ + fun getDisplayName(file: File): String } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index b75a929db45..154c8b6a840 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -167,11 +167,11 @@ class DirectoryReferenceLayerRepositoryTest { private class StubMapConfigurator : MapConfigurator { private val files = mutableMapOf>() - override fun supportsLayer(file: File?): Boolean { + override fun supportsLayer(file: File): Boolean { return files[file]!!.first } - override fun getDisplayName(file: File?): String { + override fun getDisplayName(file: File): String { return files[file]!!.second } @@ -179,21 +179,20 @@ class DirectoryReferenceLayerRepositoryTest { files[file] = Pair(isSupported, displayName) } - override fun isAvailable(context: Context?): Boolean { + override fun isAvailable(context: Context): Boolean { TODO("Not yet implemented") } - override fun showUnavailableMessage(context: Context?) { + override fun showUnavailableMessage(context: Context) { TODO("Not yet implemented") } - override fun createPrefs(context: Context?, settings: Settings?): MutableList { + override fun createPrefs(context: Context, settings: Settings): MutableList { TODO("Not yet implemented") } - override fun getPrefKeys(): MutableCollection { - TODO("Not yet implemented") - } + override val prefKeys: Collection + get() = TODO("Not yet implemented") override fun buildConfig(prefs: Settings): Bundle { TODO("Not yet implemented") From 3d81f40a5245889fdcd69c0a3457c5280b0b4559 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 4 Jun 2024 11:38:58 +0200 Subject: [PATCH 537/750] Display progress indicator when the list of layers is being refreshed --- .../maps/layers/OfflineMapLayersPicker.kt | 20 ++++++++++++------- .../layers/OfflineMapLayersPickerViewModel.kt | 6 ++++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 796562aeeeb..72979e00977 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -77,14 +77,20 @@ class OfflineMapLayersPicker( offlineMapLayersPickerBinding = OfflineMapLayersPickerBinding.inflate(inflater) viewModel.data.observe(this) { data -> - offlineMapLayersPickerBinding.progressIndicator.visibility = View.GONE - offlineMapLayersPickerBinding.layers.visibility = View.VISIBLE - offlineMapLayersPickerBinding.save.isEnabled = true - - val offlineMapLayersAdapter = OfflineMapLayersAdapter(data.first, data.second) { - viewModel.changeSelectedLayerId(it) + if (data == null) { + offlineMapLayersPickerBinding.progressIndicator.visibility = View.VISIBLE + offlineMapLayersPickerBinding.layers.visibility = View.GONE + offlineMapLayersPickerBinding.save.isEnabled = false + } else { + offlineMapLayersPickerBinding.progressIndicator.visibility = View.GONE + offlineMapLayersPickerBinding.layers.visibility = View.VISIBLE + offlineMapLayersPickerBinding.save.isEnabled = true + + val offlineMapLayersAdapter = OfflineMapLayersAdapter(data.first, data.second) { + viewModel.changeSelectedLayerId(it) + } + offlineMapLayersPickerBinding.layers.setAdapter(offlineMapLayersAdapter) } - offlineMapLayersPickerBinding.layers.setAdapter(offlineMapLayersAdapter) } offlineMapLayersPickerBinding.mbtilesInfoGroup.addOnClickListener { diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt index 95bc2ab1416..65ee45d63cf 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt @@ -12,8 +12,8 @@ class OfflineMapLayersPickerViewModel( private val scheduler: Scheduler, private val settingsProvider: SettingsProvider ) : ViewModel() { - private val _data = MutableLiveData, String?>>() - val data: LiveData, String?>> = _data + private val _data = MutableLiveData, String?>?>(null) + val data: LiveData, String?>?> = _data init { refreshLayers() @@ -29,6 +29,8 @@ class OfflineMapLayersPickerViewModel( } fun refreshLayers() { + _data.value = null + scheduler.immediate( background = { val layers = referenceLayerRepository.getAll() From 5f0b710f72aa0212d35f142b1d78af9acda94073 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 4 Jun 2024 11:43:27 +0200 Subject: [PATCH 538/750] Added new tets in OfflineMapLayersPickerTest --- maps/build.gradle.kts | 1 + .../maps/layers/OfflineMapLayersPickerTest.kt | 69 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/maps/build.gradle.kts b/maps/build.gradle.kts index 66c2fd3597b..09c5f1f8e68 100644 --- a/maps/build.gradle.kts +++ b/maps/build.gradle.kts @@ -71,4 +71,5 @@ dependencies { testImplementation(Dependencies.robolectric) testImplementation(Dependencies.mockito_kotlin) testImplementation(Dependencies.androidx_test_espresso_core) + testImplementation(Dependencies.androidx_test_espresso_intents) } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index e4e6c551b2e..eabd0a8294a 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -1,10 +1,14 @@ package org.odk.collect.maps.layers +import android.content.Intent import android.net.Uri +import androidx.core.os.bundleOf import androidx.fragment.app.testing.FragmentScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed @@ -259,6 +263,71 @@ class OfflineMapLayersPickerTest { onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(isChecked())) } + @Test + fun `clicking the 'add layer' button opens file picker that allows selecting multiple files`() { + Intents.init() + launchFragment() + + onView(withText(string.add_layer)).perform(click()) + + Intents.getIntents()[0].apply { + assertThat(this, IntentMatchers.hasAction(Intent.ACTION_GET_CONTENT)) + assertThat(categories.containsAll(listOf(Intent.CATEGORY_OPENABLE)), equalTo(true)) + assertThat(this, IntentMatchers.hasType("*/*")) + assertThat(this, IntentMatchers.hasExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)) + } + + Intents.release() + } + + @Test + fun `progress indicator is displayed during loading layers after receiving new ones`() { + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + )) + val scenario = launchFragment() + + scheduler.flush() + + scenario.onFragment { + it.childFragmentManager.setFragmentResult(OfflineMapLayersImportDialog.RESULT_KEY, bundleOf()) + } + + onView(withId(R.id.progress_indicator)).check(matches(isDisplayed())) + onView(withId(R.id.layers)).check(matches(not(isDisplayed()))) + + scheduler.flush() + onView(withId(R.id.progress_indicator)).check(matches(not(isDisplayed()))) + onView(withId(R.id.layers)).check(matches(isDisplayed())) + } + + @Test + fun `when new layers added the list should be updated`() { + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + )) + + val scenario = launchFragment() + + scheduler.flush() + + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), + ReferenceLayer("2", TempFiles.createTempFile(), "layer2") + )) + + scenario.onFragment { + it.childFragmentManager.setFragmentResult(OfflineMapLayersImportDialog.RESULT_KEY, bundleOf()) + } + + scheduler.flush() + + onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(withText(string.none))) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(withText("layer1"))) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.radio_button)).check(matches(withText("layer2"))) + } + private fun launchFragment(): FragmentScenario { return fragmentScenarioLauncherRule.launchInContainer(OfflineMapLayersPicker::class.java) } From 44860bde0da49ee97175e26fd1e6b67dd26ec816 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 4 Jun 2024 11:48:04 +0200 Subject: [PATCH 539/750] Ignor non .mbtile files --- .../maps/layers/OfflineMapLayersImportDialogViewModel.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt index 7fe15d9d5b3..eb71da83a78 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt @@ -32,10 +32,12 @@ class OfflineMapLayersImportDialogViewModel( uris?.forEach { uriString -> val uri = Uri.parse(uriString) uri.getFileName(contentResolver)?.let { fileName -> - val layerFile = File(tempLayersDir, fileName).also { file -> - uri.toFile(contentResolver, file) + if (fileName.endsWith(".mbtiles")) { + val layerFile = File(tempLayersDir, fileName).also { file -> + uri.toFile(contentResolver, file) + } + layers.add(ReferenceLayer(layerFile.absolutePath, layerFile, MbtilesFile.readName(layerFile) ?: layerFile.name)) } - layers.add(ReferenceLayer(layerFile.absolutePath, layerFile, MbtilesFile.readName(layerFile) ?: layerFile.name)) } } _data.postValue(layers) From 87a8d691e6ef97d5020215f52d33dbc0a09c8911 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 4 Jun 2024 12:11:01 +0200 Subject: [PATCH 540/750] Added tests for OfflineMapLayersImportDialog --- .../OfflineMapLayersImportDialogTest.kt | 273 ++++++++++++++++++ .../java/org/odk/collect/shared/TempFiles.kt | 2 +- 2 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogTest.kt diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogTest.kt new file mode 100644 index 00000000000..34cf4ffc152 --- /dev/null +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogTest.kt @@ -0,0 +1,273 @@ +package org.odk.collect.maps.layers + +import android.os.Bundle +import androidx.core.net.toUri +import androidx.fragment.app.testing.FragmentScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.scrollTo +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isEnabled +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.not +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.odk.collect.androidshared.ui.FragmentFactoryBuilder +import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule +import org.odk.collect.shared.TempFiles +import org.odk.collect.strings.R +import org.odk.collect.testshared.FakeScheduler +import org.odk.collect.testshared.RecyclerViewMatcher +import org.odk.collect.testshared.RobolectricHelpers +import java.io.File + +@RunWith(AndroidJUnit4::class) +class OfflineMapLayersImportDialogTest { + private val scheduler = FakeScheduler() + private val sharedLayersDirPath = TempFiles.createTempDir().absolutePath + private val projectLayersDirPath = TempFiles.createTempDir().absolutePath + + @get:Rule + val fragmentScenarioLauncherRule = FragmentScenarioLauncherRule( + FragmentFactoryBuilder() + .forClass(OfflineMapLayersImportDialog::class) { + OfflineMapLayersImportDialog(scheduler, sharedLayersDirPath, projectLayersDirPath) + }.build() + ) + + @Test + fun `clicking the 'cancel' button dismisses the dialog`() { + launchFragment(arrayListOf()).onFragment { + assertThat(it.isVisible, equalTo(true)) + onView(withText(R.string.cancel)).perform(click()) + assertThat(it.isVisible, equalTo(false)) + } + } + + @Test + fun `clicking the 'cancel' button does not set fragment result`() { + launchFragment(arrayListOf()).onFragment { + var resultReceived = false + it.parentFragmentManager.setFragmentResultListener( + OfflineMapLayersImportDialog.RESULT_KEY, + it + ) { _, _ -> + resultReceived = true + } + + onView(withId(org.odk.collect.maps.R.id.cancel_button)).perform(click()) + assertThat(resultReceived, equalTo(false)) + } + } + + @Test + fun `clicking the 'add layer' button dismisses the dialog`() { + launchFragment(arrayListOf()).onFragment { + scheduler.flush() + assertThat(it.isVisible, equalTo(true)) + onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) + scheduler.flush() + RobolectricHelpers.runLooper() + assertThat(it.isVisible, equalTo(false)) + } + } + + @Test + fun `clicking the 'add layer' button sets fragment result`() { + launchFragment(arrayListOf()).onFragment { + scheduler.flush() + var resultReceived = false + it.parentFragmentManager.setFragmentResultListener( + OfflineMapLayersImportDialog.RESULT_KEY, + it + ) { _, _ -> + resultReceived = true + } + + onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) + scheduler.flush() + RobolectricHelpers.runLooper() + assertThat(resultReceived, equalTo(true)) + } + } + + @Test + fun `progress indicator is displayed during loading layers`() { + val file1 = TempFiles.createTempFile("layer1", ".mbtiles") + val file2 = TempFiles.createTempFile("layer2", ".mbtiles") + + launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) + + onView(withId(org.odk.collect.maps.R.id.progress_indicator)).check(matches(isDisplayed())) + onView(withId(org.odk.collect.maps.R.id.layers_list)).check(matches(not(isDisplayed()))) + + scheduler.flush() + + onView(withId(org.odk.collect.maps.R.id.progress_indicator)).check(matches(not(isDisplayed()))) + onView(withId(org.odk.collect.maps.R.id.layers_list)).check(matches(isDisplayed())) + } + + @Test + fun `the 'cancel' button is enabled during loading layers`() { + launchFragment(arrayListOf()) + + onView(withId(org.odk.collect.maps.R.id.cancel_button)).check(matches(isEnabled())) + scheduler.flush() + onView(withId(org.odk.collect.maps.R.id.cancel_button)).check(matches(isEnabled())) + } + + @Test + fun `the 'add layer' button is disabled during loading layers`() { + launchFragment(arrayListOf()) + + onView(withId(org.odk.collect.maps.R.id.add_layer_button)).check(matches(not(isEnabled()))) + scheduler.flush() + onView(withId(org.odk.collect.maps.R.id.add_layer_button)).check(matches(isEnabled())) + } + + @Test + fun `'All projects' location should be selected by default`() { + launchFragment(arrayListOf()) + + onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(isChecked())) + onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(not(isChecked()))) + } + + @Test + fun `checking location sets selection correctly`() { + launchFragment(arrayListOf()) + onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(click()) + + onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(not(isChecked()))) + onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(isChecked())) + + onView(withId(org.odk.collect.maps.R.id.all_projects_option)).perform(click()) + + onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(isChecked())) + onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(not(isChecked()))) + } + + @Test + fun `recreating maintains the selected layers location`() { + val scenario = launchFragment(arrayListOf()) + onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(click()) + + scenario.recreate() + + onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(not(isChecked()))) + onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(isChecked())) + } + + @Test + fun `the list of selected layers should be displayed`() { + val file1 = TempFiles.createTempFile("layer1", ".mbtiles") + val file2 = TempFiles.createTempFile("layer2", ".mbtiles") + + launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) + + scheduler.flush() + + onView(withId(org.odk.collect.maps.R.id.layers_list)).check(matches(RecyclerViewMatcher.withListSize(2))) + onView(withText("layer1.mbtiles")).check(matches(isDisplayed())) + onView(withText("layer2.mbtiles")).check(matches(isDisplayed())) + } + + @Test + fun `recreating maintains the list of selected layers`() { + val file1 = TempFiles.createTempFile("layer1", ".mbtiles") + val file2 = TempFiles.createTempFile("layer2", ".mbtiles") + + val scenario = launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) + scheduler.flush() + + scenario.recreate() + + onView(withId(org.odk.collect.maps.R.id.layers_list)).check(matches(RecyclerViewMatcher.withListSize(2))) + onView(withText("layer1.mbtiles")).check(matches(isDisplayed())) + onView(withText("layer2.mbtiles")).check(matches(isDisplayed())) + } + + @Test + fun `only mbtiles files are taken into account`() { + val file1 = TempFiles.createTempFile("layer1", ".mbtiles") + val file2 = TempFiles.createTempFile("layer2", ".txt") + + launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) + scheduler.flush() + + onView(withId(org.odk.collect.maps.R.id.layers_list)).check(matches(RecyclerViewMatcher.withListSize(1))) + onView(withText("layer1.mbtiles")).check(matches(isDisplayed())) + onView(withText("layer2.txt")).check(doesNotExist()) + } + + @Test + fun `clicking the 'add layer' button moves the files to the shared layers dir if it is selected`() { + val file1 = TempFiles.createTempFile("layer1", ".mbtiles").also { + it.writeText("blah1") + } + val file2 = TempFiles.createTempFile("layer2", ".mbtiles").also { + it.writeText("blah2") + } + + launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) + scheduler.flush() + + onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) + scheduler.flush() + + assertThat(File(sharedLayersDirPath).listFiles().size, equalTo(2)) + assertThat(File(projectLayersDirPath).listFiles().size, equalTo(0)) + + val copiedFile1 = File(sharedLayersDirPath, "layer1.mbtiles") + assertThat(copiedFile1.exists(), equalTo(true)) + assertThat(copiedFile1.readText(), equalTo("blah1")) + + val copiedFile2 = File(sharedLayersDirPath, "layer2.mbtiles") + assertThat(copiedFile2.exists(), equalTo(true)) + assertThat(copiedFile2.readText(), equalTo("blah2")) + } + + @Test + fun `clicking the 'add layer' button moves the files to the project layers dir if it is selected`() { + val file1 = TempFiles.createTempFile("layer1", ".mbtiles").also { + it.writeText("blah1") + } + val file2 = TempFiles.createTempFile("layer2", ".mbtiles").also { + it.writeText("blah2") + } + + launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) + scheduler.flush() + + onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(scrollTo(), click()) + + onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) + scheduler.flush() + + assertThat(File(sharedLayersDirPath).listFiles().size, equalTo(0)) + assertThat(File(projectLayersDirPath).listFiles().size, equalTo(2)) + + val copiedFile1 = File(projectLayersDirPath, "layer1.mbtiles") + assertThat(copiedFile1.exists(), equalTo(true)) + assertThat(copiedFile1.readText(), equalTo("blah1")) + + val copiedFile2 = File(projectLayersDirPath, "layer2.mbtiles") + assertThat(copiedFile2.exists(), equalTo(true)) + assertThat(copiedFile2.readText(), equalTo("blah2")) + } + + private fun launchFragment(uris: ArrayList): FragmentScenario { + return fragmentScenarioLauncherRule.launchInContainer( + OfflineMapLayersImportDialog::class.java, + Bundle().apply { putStringArrayList(OfflineMapLayersImportDialog.URIS, uris) } + ) + } +} diff --git a/shared/src/main/java/org/odk/collect/shared/TempFiles.kt b/shared/src/main/java/org/odk/collect/shared/TempFiles.kt index f5b8fb422e1..4c4bde65b64 100644 --- a/shared/src/main/java/org/odk/collect/shared/TempFiles.kt +++ b/shared/src/main/java/org/odk/collect/shared/TempFiles.kt @@ -26,7 +26,7 @@ object TempFiles { @JvmStatic fun createTempFile(name: String, extension: String): File { val tmpDir = getTempDir() - return File(tmpDir, name + getRandomName(tmpDir) + extension).also { + return File(tmpDir, name + extension).also { it.createNewFile() it.deleteOnExit() } From fa473d1d89bd8c95dd69e80715751ec096b1672c Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 4 Jun 2024 12:13:54 +0200 Subject: [PATCH 541/750] Created a const for the mbtiles file extension --- .../odk/collect/maps/layers/MbtilesFile.java | 4 +++- .../OfflineMapLayersImportDialogViewModel.kt | 2 +- .../OfflineMapLayersImportDialogTest.kt | 22 +++++++++---------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/MbtilesFile.java b/maps/src/main/java/org/odk/collect/maps/layers/MbtilesFile.java index 0e15ba40ba1..8dfbb202e8a 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/MbtilesFile.java +++ b/maps/src/main/java/org/odk/collect/maps/layers/MbtilesFile.java @@ -28,6 +28,8 @@ * See https://github.com/mapbox/mbtiles-spec for the detailed specification. */ public class MbtilesFile implements Closeable, TileSource { + public static final String FILE_EXTENSION = ".mbtiles"; + public enum LayerType { RASTER, VECTOR } private final File file; @@ -166,7 +168,7 @@ private static String detectContentType(File file) throws MbtilesException { if (!file.exists() || !file.isFile()) { throw new NotFileException(file); } - if (!file.getName().toLowerCase(Locale.US).endsWith(".mbtiles")) { + if (!file.getName().toLowerCase(Locale.US).endsWith(FILE_EXTENSION)) { throw new UnsupportedFilenameException(file); } try (SQLiteDatabase db = openSqliteReadOnly(file)) { diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt index eb71da83a78..51388b6d459 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt @@ -32,7 +32,7 @@ class OfflineMapLayersImportDialogViewModel( uris?.forEach { uriString -> val uri = Uri.parse(uriString) uri.getFileName(contentResolver)?.let { fileName -> - if (fileName.endsWith(".mbtiles")) { + if (fileName.endsWith(MbtilesFile.FILE_EXTENSION)) { val layerFile = File(tempLayersDir, fileName).also { file -> uri.toFile(contentResolver, file) } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogTest.kt index 34cf4ffc152..369a3d2176a 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogTest.kt @@ -101,8 +101,8 @@ class OfflineMapLayersImportDialogTest { @Test fun `progress indicator is displayed during loading layers`() { - val file1 = TempFiles.createTempFile("layer1", ".mbtiles") - val file2 = TempFiles.createTempFile("layer2", ".mbtiles") + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) @@ -168,8 +168,8 @@ class OfflineMapLayersImportDialogTest { @Test fun `the list of selected layers should be displayed`() { - val file1 = TempFiles.createTempFile("layer1", ".mbtiles") - val file2 = TempFiles.createTempFile("layer2", ".mbtiles") + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) @@ -182,8 +182,8 @@ class OfflineMapLayersImportDialogTest { @Test fun `recreating maintains the list of selected layers`() { - val file1 = TempFiles.createTempFile("layer1", ".mbtiles") - val file2 = TempFiles.createTempFile("layer2", ".mbtiles") + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) val scenario = launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) scheduler.flush() @@ -197,7 +197,7 @@ class OfflineMapLayersImportDialogTest { @Test fun `only mbtiles files are taken into account`() { - val file1 = TempFiles.createTempFile("layer1", ".mbtiles") + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) val file2 = TempFiles.createTempFile("layer2", ".txt") launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) @@ -210,10 +210,10 @@ class OfflineMapLayersImportDialogTest { @Test fun `clicking the 'add layer' button moves the files to the shared layers dir if it is selected`() { - val file1 = TempFiles.createTempFile("layer1", ".mbtiles").also { + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION).also { it.writeText("blah1") } - val file2 = TempFiles.createTempFile("layer2", ".mbtiles").also { + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION).also { it.writeText("blah2") } @@ -237,10 +237,10 @@ class OfflineMapLayersImportDialogTest { @Test fun `clicking the 'add layer' button moves the files to the project layers dir if it is selected`() { - val file1 = TempFiles.createTempFile("layer1", ".mbtiles").also { + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION).also { it.writeText("blah1") } - val file2 = TempFiles.createTempFile("layer2", ".mbtiles").also { + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION).also { it.writeText("blah2") } From 686bb5bbae8a404e13a2189c9cf40fc439258062 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 4 Jun 2024 12:17:35 +0200 Subject: [PATCH 542/750] Fixed the section about modules in CODE-GUIDELINES --- docs/CODE-GUIDELINES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/CODE-GUIDELINES.md b/docs/CODE-GUIDELINES.md index 754eea08896..4870b38cd90 100644 --- a/docs/CODE-GUIDELINES.md +++ b/docs/CODE-GUIDELINES.md @@ -178,12 +178,12 @@ Collect is a multi module Gradle project. Modules should have a focused feature There's no easy way to define exactly when a new module should be pulled out of an existing one or when new code calls for a new module - it's best to discuss that with the team before making any decisions. Once a structure has been agreed on, to add a new module: 1. Click `File > New > New module...` in Android Studio -1. Decide whether the new module should be an "Android Library" or "Java or Kotlin Library" - ideally as much code as possible could avoid relying on Android but a lot of features will require at least one Android Library module -1. Review the generated `build.gradle` and remove any unnecessary dependencies or setup -1. Add quality checks to the module's `build.gradle`: +2. Decide whether the new module should be an "Android Library" or "Java or Kotlin Library" - ideally as much code as possible could avoid relying on Android but a lot of features will require at least one Android Library module +3. Review the generated `build.gradle` and remove any unnecessary dependencies or setup +4. Add quality checks to the module's `build.gradle`: ``` apply from: '../config/quality.gradle' ``` -1. If the module will have tests, make sure they get run on CI by adding a line to `test_modules.txt` with `:test` for a Java Library or `:testDebug` for an Android library +5. If the module will have tests, make sure they get run on CI by adding a line to `test_modules.txt` with `` From e3c54044c0696009f47862f1c6bce2ce67d28b66 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 4 Jun 2024 12:30:02 +0200 Subject: [PATCH 543/750] Naming improvements --- ...tDialog.kt => OfflineMapLayersImporter.kt} | 18 ++++++------- ....kt => OfflineMapLayersImporterAdapter.kt} | 10 +++---- ...t => OfflineMapLayersImporterViewModel.kt} | 2 +- .../maps/layers/OfflineMapLayersPicker.kt | 15 +++++------ ...er.kt => OfflineMapLayersPickerAdapter.kt} | 10 +++---- ...og.xml => offline_map_layers_importer.xml} | 4 +-- ...l => offline_map_layers_importer_item.xml} | 0 ...xml => offline_map_layers_picker_item.xml} | 0 ...est.kt => OfflineMapLayersImporterTest.kt} | 26 +++++++++---------- .../maps/layers/OfflineMapLayersPickerTest.kt | 4 +-- 10 files changed, 44 insertions(+), 45 deletions(-) rename maps/src/main/java/org/odk/collect/maps/layers/{OfflineMapLayersImportDialog.kt => OfflineMapLayersImporter.kt} (80%) rename maps/src/main/java/org/odk/collect/maps/layers/{OfflineMapLayersImportAdapter.kt => OfflineMapLayersImporterAdapter.kt} (55%) rename maps/src/main/java/org/odk/collect/maps/layers/{OfflineMapLayersImportDialogViewModel.kt => OfflineMapLayersImporterViewModel.kt} (98%) rename maps/src/main/java/org/odk/collect/maps/layers/{OfflineMapLayersAdapter.kt => OfflineMapLayersPickerAdapter.kt} (77%) rename maps/src/main/res/layout/{offline_map_layers_import_dialog.xml => offline_map_layers_importer.xml} (98%) rename maps/src/main/res/layout/{offline_map_layers_import_item.xml => offline_map_layers_importer_item.xml} (100%) rename maps/src/main/res/layout/{offline_map_layer.xml => offline_map_layers_picker_item.xml} (100%) rename maps/src/test/java/org/odk/collect/maps/layers/{OfflineMapLayersImportDialogTest.kt => OfflineMapLayersImporterTest.kt} (91%) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialog.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt similarity index 80% rename from maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialog.kt rename to maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt index da7b38dc638..cc93d5d595d 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialog.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt @@ -11,40 +11,40 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import org.odk.collect.async.Scheduler -import org.odk.collect.maps.databinding.OfflineMapLayersImportDialogBinding +import org.odk.collect.maps.databinding.OfflineMapLayersImporterBinding import org.odk.collect.material.MaterialFullScreenDialogFragment import org.odk.collect.material.MaterialProgressDialogFragment -class OfflineMapLayersImportDialog( +class OfflineMapLayersImporter( private val scheduler: Scheduler, private val sharedLayersDirPath: String, private val projectLayersDirPath: String ) : MaterialFullScreenDialogFragment() { - private val viewModel: OfflineMapLayersImportDialogViewModel by viewModels { + private val viewModel: OfflineMapLayersImporterViewModel by viewModels { object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return OfflineMapLayersImportDialogViewModel(scheduler, requireContext().contentResolver) as T + return OfflineMapLayersImporterViewModel(scheduler, requireContext().contentResolver) as T } } } - private lateinit var binding: OfflineMapLayersImportDialogBinding + private lateinit var binding: OfflineMapLayersImporterBinding override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = OfflineMapLayersImportDialogBinding.inflate(inflater) + binding = OfflineMapLayersImporterBinding.inflate(inflater) viewModel.data.observe(this) { data -> binding.progressIndicator.visibility = View.GONE - binding.layersList.visibility = View.VISIBLE + binding.layers.visibility = View.VISIBLE binding.addLayerButton.isEnabled = true - val adapter = OfflineMapLayersImportAdapter(data) - binding.layersList.setAdapter(adapter) + val adapter = OfflineMapLayersImporterAdapter(data) + binding.layers.setAdapter(adapter) } viewModel.init(requireArguments().getStringArrayList(URIS)) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportAdapter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterAdapter.kt similarity index 55% rename from maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportAdapter.kt rename to maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterAdapter.kt index 154a7940432..d011f7e8d61 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportAdapter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterAdapter.kt @@ -3,14 +3,14 @@ package org.odk.collect.maps.layers import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import org.odk.collect.maps.databinding.OfflineMapLayersImportItemBinding +import org.odk.collect.maps.databinding.OfflineMapLayersImporterItemBinding -class OfflineMapLayersImportAdapter( +class OfflineMapLayersImporterAdapter( private val layers: List, -) : RecyclerView.Adapter() { +) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val binding = OfflineMapLayersImportItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = OfflineMapLayersImporterItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) return ViewHolder(binding) } @@ -20,5 +20,5 @@ class OfflineMapLayersImportAdapter( override fun getItemCount() = layers.size - class ViewHolder(val binding: OfflineMapLayersImportItemBinding) : RecyclerView.ViewHolder(binding.root) + class ViewHolder(val binding: OfflineMapLayersImporterItemBinding) : RecyclerView.ViewHolder(binding.root) } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterViewModel.kt similarity index 98% rename from maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt rename to maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterViewModel.kt index 51388b6d459..02f1c807643 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImportDialogViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterViewModel.kt @@ -12,7 +12,7 @@ import org.odk.collect.shared.TempFiles import java.io.File import java.util.ArrayList -class OfflineMapLayersImportDialogViewModel( +class OfflineMapLayersImporterViewModel( private val scheduler: Scheduler, private val contentResolver: ContentResolver ) : ViewModel() { diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 72979e00977..4bd8dcdb2d6 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -6,7 +6,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts -import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -44,8 +43,8 @@ class OfflineMapLayersPicker( } DialogFragmentUtils.showIfNotShowing( - OfflineMapLayersImportDialog::class.java, - Bundle().apply { putStringArrayList(OfflineMapLayersImportDialog.URIS, ArrayList(uriStrings)) }, + OfflineMapLayersImporter::class.java, + Bundle().apply { putStringArrayList(OfflineMapLayersImporter.URIS, ArrayList(uriStrings)) }, childFragmentManager ) } @@ -53,8 +52,8 @@ class OfflineMapLayersPicker( override fun onCreate(savedInstanceState: Bundle?) { childFragmentManager.fragmentFactory = FragmentFactoryBuilder() - .forClass(OfflineMapLayersImportDialog::class) { - OfflineMapLayersImportDialog( + .forClass(OfflineMapLayersImporter::class) { + OfflineMapLayersImporter( scheduler, referenceLayerRepository.getSharedLayersDirPath(), referenceLayerRepository.getProjectLayersDirPath() @@ -64,7 +63,7 @@ class OfflineMapLayersPicker( super.onCreate(savedInstanceState) - childFragmentManager.setFragmentResultListener(OfflineMapLayersImportDialog.RESULT_KEY, this) { _, _ -> + childFragmentManager.setFragmentResultListener(OfflineMapLayersImporter.RESULT_KEY, this) { _, _ -> viewModel.refreshLayers() } } @@ -86,10 +85,10 @@ class OfflineMapLayersPicker( offlineMapLayersPickerBinding.layers.visibility = View.VISIBLE offlineMapLayersPickerBinding.save.isEnabled = true - val offlineMapLayersAdapter = OfflineMapLayersAdapter(data.first, data.second) { + val adapter = OfflineMapLayersPickerAdapter(data.first, data.second) { viewModel.changeSelectedLayerId(it) } - offlineMapLayersPickerBinding.layers.setAdapter(offlineMapLayersAdapter) + offlineMapLayersPickerBinding.layers.setAdapter(adapter) } } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersAdapter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt similarity index 77% rename from maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersAdapter.kt rename to maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt index 18896722cf4..e4be8ce6bdb 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersAdapter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt @@ -3,17 +3,17 @@ package org.odk.collect.maps.layers import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import org.odk.collect.maps.databinding.OfflineMapLayerBinding +import org.odk.collect.maps.databinding.OfflineMapLayersPickerItemBinding import org.odk.collect.strings.localization.getLocalizedString -class OfflineMapLayersAdapter( +class OfflineMapLayersPickerAdapter( private val layers: List, private var selectedLayerId: String?, private val onSelectedLayerChanged: (String?) -> Unit -) : RecyclerView.Adapter() { +) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val binding = OfflineMapLayerBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = OfflineMapLayersPickerItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) return ViewHolder(binding) } @@ -41,5 +41,5 @@ class OfflineMapLayersAdapter( override fun getItemCount() = layers.size + 1 - class ViewHolder(val binding: OfflineMapLayerBinding) : RecyclerView.ViewHolder(binding.root) + class ViewHolder(val binding: OfflineMapLayersPickerItemBinding) : RecyclerView.ViewHolder(binding.root) } diff --git a/maps/src/main/res/layout/offline_map_layers_import_dialog.xml b/maps/src/main/res/layout/offline_map_layers_importer.xml similarity index 98% rename from maps/src/main/res/layout/offline_map_layers_import_dialog.xml rename to maps/src/main/res/layout/offline_map_layers_importer.xml index 537baf20bbf..dbc456424e8 100644 --- a/maps/src/main/res/layout/offline_map_layers_import_dialog.xml +++ b/maps/src/main/res/layout/offline_map_layers_importer.xml @@ -51,7 +51,7 @@ app:layout_constraintEnd_toEndOf="parent" /> + app:constraint_referenced_ids="layers,progress_indicator" /> resultReceived = true @@ -86,7 +86,7 @@ class OfflineMapLayersImportDialogTest { scheduler.flush() var resultReceived = false it.parentFragmentManager.setFragmentResultListener( - OfflineMapLayersImportDialog.RESULT_KEY, + OfflineMapLayersImporter.RESULT_KEY, it ) { _, _ -> resultReceived = true @@ -107,12 +107,12 @@ class OfflineMapLayersImportDialogTest { launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) onView(withId(org.odk.collect.maps.R.id.progress_indicator)).check(matches(isDisplayed())) - onView(withId(org.odk.collect.maps.R.id.layers_list)).check(matches(not(isDisplayed()))) + onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(not(isDisplayed()))) scheduler.flush() onView(withId(org.odk.collect.maps.R.id.progress_indicator)).check(matches(not(isDisplayed()))) - onView(withId(org.odk.collect.maps.R.id.layers_list)).check(matches(isDisplayed())) + onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(isDisplayed())) } @Test @@ -175,7 +175,7 @@ class OfflineMapLayersImportDialogTest { scheduler.flush() - onView(withId(org.odk.collect.maps.R.id.layers_list)).check(matches(RecyclerViewMatcher.withListSize(2))) + onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) onView(withText("layer1.mbtiles")).check(matches(isDisplayed())) onView(withText("layer2.mbtiles")).check(matches(isDisplayed())) } @@ -190,7 +190,7 @@ class OfflineMapLayersImportDialogTest { scenario.recreate() - onView(withId(org.odk.collect.maps.R.id.layers_list)).check(matches(RecyclerViewMatcher.withListSize(2))) + onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) onView(withText("layer1.mbtiles")).check(matches(isDisplayed())) onView(withText("layer2.mbtiles")).check(matches(isDisplayed())) } @@ -203,7 +203,7 @@ class OfflineMapLayersImportDialogTest { launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) scheduler.flush() - onView(withId(org.odk.collect.maps.R.id.layers_list)).check(matches(RecyclerViewMatcher.withListSize(1))) + onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(1))) onView(withText("layer1.mbtiles")).check(matches(isDisplayed())) onView(withText("layer2.txt")).check(doesNotExist()) } @@ -264,10 +264,10 @@ class OfflineMapLayersImportDialogTest { assertThat(copiedFile2.readText(), equalTo("blah2")) } - private fun launchFragment(uris: ArrayList): FragmentScenario { + private fun launchFragment(uris: ArrayList): FragmentScenario { return fragmentScenarioLauncherRule.launchInContainer( - OfflineMapLayersImportDialog::class.java, - Bundle().apply { putStringArrayList(OfflineMapLayersImportDialog.URIS, uris) } + OfflineMapLayersImporter::class.java, + Bundle().apply { putStringArrayList(OfflineMapLayersImporter.URIS, uris) } ) } } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index eabd0a8294a..7378a82fdae 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -290,7 +290,7 @@ class OfflineMapLayersPickerTest { scheduler.flush() scenario.onFragment { - it.childFragmentManager.setFragmentResult(OfflineMapLayersImportDialog.RESULT_KEY, bundleOf()) + it.childFragmentManager.setFragmentResult(OfflineMapLayersImporter.RESULT_KEY, bundleOf()) } onView(withId(R.id.progress_indicator)).check(matches(isDisplayed())) @@ -317,7 +317,7 @@ class OfflineMapLayersPickerTest { )) scenario.onFragment { - it.childFragmentManager.setFragmentResult(OfflineMapLayersImportDialog.RESULT_KEY, bundleOf()) + it.childFragmentManager.setFragmentResult(OfflineMapLayersImporter.RESULT_KEY, bundleOf()) } scheduler.flush() From ee1333eb32760f53cfa234d7204fe2f7d8fca3c2 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 4 Jun 2024 12:36:44 +0100 Subject: [PATCH 544/750] Output new APK size when checking fails --- .circleci/config.yml | 2 +- check-size.sh | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100755 check-size.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 008bc1b04f4..e7cb2086e74 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -231,7 +231,7 @@ jobs: - run: name: Check APK size isn't larger than 10.9MB - command: if [ $(ls -l collect_app/build/outputs/apk/selfSignedRelease/*.apk | awk '{print $5}') -gt 10900000 ]; then exit 1; fi + command: ./check-size.sh - run: name: Copy APK to predictable path for artifact storage diff --git a/check-size.sh b/check-size.sh new file mode 100755 index 00000000000..dfe569f3b5d --- /dev/null +++ b/check-size.sh @@ -0,0 +1,6 @@ +set -e + +if [ $(ls -l collect_app/build/outputs/apk/selfSignedRelease/*.apk | awk '{print $5}') -gt 10900000 ];then + echo "APK increased to $(ls -l collect_app/build/outputs/apk/selfSignedRelease/*.apk | awk '{print $5}') bytes!" + exit 1 +fi From 76f344c2e94fa306e76fcd4e4a92561a3df24354 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 4 Jun 2024 12:49:37 +0100 Subject: [PATCH 545/750] Update cache key calculation --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e7cb2086e74..fc2eb9db11a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,7 +17,7 @@ jobs: - checkout - run: name: Generate combined build.gradle file for cache key - command: cat build.gradle */build.gradle .circleci/gradle.properties .circleci/config.yml buildSrc/src/main/java/dependencies/Dependencies.kt buildSrc/src/main/java/dependencies/Versions.kt > deps.txt + command: cat build.gradle */build.gradle */build.gradle.kts .circleci/gradle.properties .circleci/config.yml buildSrc/src/main/java/dependencies/Dependencies.kt buildSrc/src/main/java/dependencies/Versions.kt > deps.txt - restore_cache: keys: - compile-deps-{{ checksum "deps.txt" }} From dcbd423a1a02e7e96209492bb02021acbb1039f3 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 4 Jun 2024 13:48:12 +0100 Subject: [PATCH 546/750] Increase APK size limit --- .circleci/config.yml | 2 +- check-size.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fc2eb9db11a..21a441e12ce 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -230,7 +230,7 @@ jobs: command: ./gradlew assembleSelfSignedRelease - run: - name: Check APK size isn't larger than 10.9MB + name: Check APK size isn't larger than 11.5MB command: ./check-size.sh - run: diff --git a/check-size.sh b/check-size.sh index dfe569f3b5d..0c31a5c5433 100755 --- a/check-size.sh +++ b/check-size.sh @@ -1,6 +1,6 @@ set -e -if [ $(ls -l collect_app/build/outputs/apk/selfSignedRelease/*.apk | awk '{print $5}') -gt 10900000 ];then +if [ $(ls -l collect_app/build/outputs/apk/selfSignedRelease/*.apk | awk '{print $5}') -gt 11500000 ];then echo "APK increased to $(ls -l collect_app/build/outputs/apk/selfSignedRelease/*.apk | awk '{print $5}') bytes!" exit 1 fi From 8392808c06a2b9fb4c8257cd80691e933db018a0 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 5 Jun 2024 13:11:44 +0200 Subject: [PATCH 547/750] Improved OfflineMapLayersPickerTest --- .../screens/MapsPreferencesFragment.kt | 2 +- .../geo/geopoint/GeoPointMapActivity.java | 2 +- .../collect/geo/geopoly/GeoPolyActivity.java | 2 +- .../geo/selection/SelectionMapFragment.kt | 2 +- maps/build.gradle.kts | 1 - .../maps/layers/OfflineMapLayersPicker.kt | 4 +- .../maps/layers/OfflineMapLayersPickerTest.kt | 56 ++++++++++++++----- 7 files changed, 50 insertions(+), 19 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt index 9bfa847b244..b6948878a19 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt @@ -56,7 +56,7 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP override fun onCreate(savedInstanceState: Bundle?) { childFragmentManager.fragmentFactory = FragmentFactoryBuilder() .forClass(OfflineMapLayersPicker::class) { - OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) + OfflineMapLayersPicker(requireActivity().activityResultRegistry, referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) } .build() diff --git a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java index b731fc95187..071e441d7e1 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java +++ b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java @@ -141,7 +141,7 @@ public void onCreate(Bundle savedInstanceState) { getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() .forClass(MapFragment.class, () -> (Fragment) mapFragmentFactory.createMapFragment()) - .forClass(OfflineMapLayersPicker.class, () -> new OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper)) + .forClass(OfflineMapLayersPicker.class, () -> new OfflineMapLayersPicker(getActivityResultRegistry(), referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper)) .build() ); diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java index 634377fdf43..ae842ddf123 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java @@ -154,7 +154,7 @@ public void handleOnBackPressed() { getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() .forClass(MapFragment.class, () -> (Fragment) mapFragmentFactory.createMapFragment()) - .forClass(OfflineMapLayersPicker.class, () -> new OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper)) + .forClass(OfflineMapLayersPicker.class, () -> new OfflineMapLayersPicker(getActivityResultRegistry(), referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper)) .build() ); diff --git a/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt b/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt index 3ddb596d2f7..1aebdbe9a67 100644 --- a/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt @@ -97,7 +97,7 @@ class SelectionMapFragment( mapFragmentFactory.createMapFragment() as Fragment } .forClass(OfflineMapLayersPicker::class) { - OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) + OfflineMapLayersPicker(requireActivity().activityResultRegistry, referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) } .build() diff --git a/maps/build.gradle.kts b/maps/build.gradle.kts index 09c5f1f8e68..66c2fd3597b 100644 --- a/maps/build.gradle.kts +++ b/maps/build.gradle.kts @@ -71,5 +71,4 @@ dependencies { testImplementation(Dependencies.robolectric) testImplementation(Dependencies.mockito_kotlin) testImplementation(Dependencies.androidx_test_espresso_core) - testImplementation(Dependencies.androidx_test_espresso_intents) } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 4bd8dcdb2d6..0a004c2a8ac 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel @@ -20,6 +21,7 @@ import org.odk.collect.settings.SettingsProvider import org.odk.collect.webpage.ExternalWebPageHelper class OfflineMapLayersPicker( + registry: ActivityResultRegistry, private val referenceLayerRepository: ReferenceLayerRepository, private val scheduler: Scheduler, private val settingsProvider: SettingsProvider, @@ -35,7 +37,7 @@ class OfflineMapLayersPicker( private lateinit var offlineMapLayersPickerBinding: OfflineMapLayersPickerBinding - private val getLayers = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris -> + private val getLayers = registerForActivityResult(ActivityResultContracts.GetMultipleContents(), registry) { uris -> if (uris.isNotEmpty()) { val uriStrings: MutableList = ArrayList() for (uri in uris) { diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index 7378a82fdae..7d523a15226 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -1,14 +1,15 @@ package org.odk.collect.maps.layers -import android.content.Intent import android.net.Uri +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.app.ActivityOptionsCompat import androidx.core.os.bundleOf import androidx.fragment.app.testing.FragmentScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.intent.Intents -import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed @@ -17,6 +18,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.instanceOf import org.hamcrest.CoreMatchers.not import org.junit.Rule import org.junit.Test @@ -43,16 +45,32 @@ import org.odk.collect.webpage.ExternalWebPageHelper class OfflineMapLayersPickerTest { private val referenceLayerRepository = mock().also { whenever(it.getAll()).thenReturn(emptyList()) + whenever(it.getSharedLayersDirPath()).thenReturn("") + whenever(it.getProjectLayersDirPath()).thenReturn("") } private val scheduler = FakeScheduler() private val settingsProvider = InMemSettingsProvider() private val externalWebPageHelper = mock() + private val uris = mutableListOf() + private val testRegistry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) { + assertThat(contract, instanceOf(ActivityResultContracts.GetMultipleContents()::class.java)) + assertThat(input, equalTo("*/*")) + dispatchResult(requestCode, uris) + } + } + @get:Rule val fragmentScenarioLauncherRule = FragmentScenarioLauncherRule( FragmentFactoryBuilder() .forClass(OfflineMapLayersPicker::class) { - OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) + OfflineMapLayersPicker(testRegistry, referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) }.build() ) @@ -264,20 +282,32 @@ class OfflineMapLayersPickerTest { } @Test - fun `clicking the 'add layer' button opens file picker that allows selecting multiple files`() { - Intents.init() - launchFragment() + fun `clicking the 'add layer' and selecting layers displays the confirmation dialog`() { + val scenario = launchFragment() + uris.add(Uri.parse("blah")) onView(withText(string.add_layer)).perform(click()) - Intents.getIntents()[0].apply { - assertThat(this, IntentMatchers.hasAction(Intent.ACTION_GET_CONTENT)) - assertThat(categories.containsAll(listOf(Intent.CATEGORY_OPENABLE)), equalTo(true)) - assertThat(this, IntentMatchers.hasType("*/*")) - assertThat(this, IntentMatchers.hasExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)) + scenario.onFragment { + assertThat( + it.childFragmentManager.findFragmentByTag(OfflineMapLayersImporter::class.java.name), + instanceOf(OfflineMapLayersImporter::class.java) + ) } + } - Intents.release() + @Test + fun `clicking the 'add layer' and selecting nothing does not display the confirmation dialog`() { + val scenario = launchFragment() + + onView(withText(string.add_layer)).perform(click()) + + scenario.onFragment { + assertThat( + it.childFragmentManager.findFragmentByTag(OfflineMapLayersImporter::class.java.name), + equalTo(null) + ) + } } @Test From 78bfdc5db5d50e0bc660ce6f4578f6b5b056e486 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 5 Jun 2024 13:19:36 +0200 Subject: [PATCH 548/750] Improved CODE_GUIDELINES.md --- docs/CODE-GUIDELINES.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/CODE-GUIDELINES.md b/docs/CODE-GUIDELINES.md index 4870b38cd90..c2daebe7584 100644 --- a/docs/CODE-GUIDELINES.md +++ b/docs/CODE-GUIDELINES.md @@ -186,4 +186,10 @@ There's no easy way to define exactly when a new module should be pulled out of apply from: '../config/quality.gradle' ``` -5. If the module will have tests, make sure they get run on CI by adding a line to `test_modules.txt` with `` +5. If the module will have tests, make sure they get run on CI by adding a line to `test_modules.txt` with `` and if it's a non-Android module, registering the `testDebug` task in its `build.gradle` file: + + ``` + tasks.register("testDebug") { + dependsOn("test") + } + ``` From c07020d1227e36308920aeb9e131f637db68004a Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 5 Jun 2024 13:34:22 +0200 Subject: [PATCH 549/750] Reverted the change in createTempFile() method --- .../layers/OfflineMapLayersImporterTest.kt | 20 +++++++++---------- .../java/org/odk/collect/shared/TempFiles.kt | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt index c8215f477f4..b8564323800 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt @@ -176,8 +176,8 @@ class OfflineMapLayersImporterTest { scheduler.flush() onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) - onView(withText("layer1.mbtiles")).check(matches(isDisplayed())) - onView(withText("layer2.mbtiles")).check(matches(isDisplayed())) + onView(withText(file1.name)).check(matches(isDisplayed())) + onView(withText(file2.name)).check(matches(isDisplayed())) } @Test @@ -191,8 +191,8 @@ class OfflineMapLayersImporterTest { scenario.recreate() onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) - onView(withText("layer1.mbtiles")).check(matches(isDisplayed())) - onView(withText("layer2.mbtiles")).check(matches(isDisplayed())) + onView(withText(file1.name)).check(matches(isDisplayed())) + onView(withText(file2.name)).check(matches(isDisplayed())) } @Test @@ -204,8 +204,8 @@ class OfflineMapLayersImporterTest { scheduler.flush() onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(1))) - onView(withText("layer1.mbtiles")).check(matches(isDisplayed())) - onView(withText("layer2.txt")).check(doesNotExist()) + onView(withText(file1.name)).check(matches(isDisplayed())) + onView(withText(file2.name)).check(doesNotExist()) } @Test @@ -226,11 +226,11 @@ class OfflineMapLayersImporterTest { assertThat(File(sharedLayersDirPath).listFiles().size, equalTo(2)) assertThat(File(projectLayersDirPath).listFiles().size, equalTo(0)) - val copiedFile1 = File(sharedLayersDirPath, "layer1.mbtiles") + val copiedFile1 = File(sharedLayersDirPath, file1.name) assertThat(copiedFile1.exists(), equalTo(true)) assertThat(copiedFile1.readText(), equalTo("blah1")) - val copiedFile2 = File(sharedLayersDirPath, "layer2.mbtiles") + val copiedFile2 = File(sharedLayersDirPath, file2.name) assertThat(copiedFile2.exists(), equalTo(true)) assertThat(copiedFile2.readText(), equalTo("blah2")) } @@ -255,11 +255,11 @@ class OfflineMapLayersImporterTest { assertThat(File(sharedLayersDirPath).listFiles().size, equalTo(0)) assertThat(File(projectLayersDirPath).listFiles().size, equalTo(2)) - val copiedFile1 = File(projectLayersDirPath, "layer1.mbtiles") + val copiedFile1 = File(projectLayersDirPath, file1.name) assertThat(copiedFile1.exists(), equalTo(true)) assertThat(copiedFile1.readText(), equalTo("blah1")) - val copiedFile2 = File(projectLayersDirPath, "layer2.mbtiles") + val copiedFile2 = File(projectLayersDirPath, file2.name) assertThat(copiedFile2.exists(), equalTo(true)) assertThat(copiedFile2.readText(), equalTo("blah2")) } diff --git a/shared/src/main/java/org/odk/collect/shared/TempFiles.kt b/shared/src/main/java/org/odk/collect/shared/TempFiles.kt index 4c4bde65b64..f5b8fb422e1 100644 --- a/shared/src/main/java/org/odk/collect/shared/TempFiles.kt +++ b/shared/src/main/java/org/odk/collect/shared/TempFiles.kt @@ -26,7 +26,7 @@ object TempFiles { @JvmStatic fun createTempFile(name: String, extension: String): File { val tmpDir = getTempDir() - return File(tmpDir, name + extension).also { + return File(tmpDir, name + getRandomName(tmpDir) + extension).also { it.createNewFile() it.deleteOnExit() } From b91a106aa26690d06e1e720bd1ddae602db76be6 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 5 Jun 2024 14:59:12 +0200 Subject: [PATCH 550/750] Fixed observations in OfflineMapLayersImporter --- .../maps/layers/OfflineMapLayersImporter.kt | 52 +++++++++++-------- .../OfflineMapLayersImporterViewModel.kt | 15 ++++-- .../layout/offline_map_layers_importer.xml | 21 +------- .../layers/OfflineMapLayersImporterTest.kt | 31 ++++++++--- 4 files changed, 66 insertions(+), 53 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt index cc93d5d595d..ec598ee1432 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt @@ -38,14 +38,6 @@ class OfflineMapLayersImporter( ): View { binding = OfflineMapLayersImporterBinding.inflate(inflater) - viewModel.data.observe(this) { data -> - binding.progressIndicator.visibility = View.GONE - binding.layers.visibility = View.VISIBLE - binding.addLayerButton.isEnabled = true - - val adapter = OfflineMapLayersImporterAdapter(data) - binding.layers.setAdapter(adapter) - } viewModel.init(requireArguments().getStringArrayList(URIS)) binding.cancelButton.setOnClickListener { @@ -59,25 +51,39 @@ class OfflineMapLayersImporter( projectLayersDirPath } - val isLoading = viewModel.addLayers(layersDir) - MaterialProgressDialogFragment.showOn( - this, - isLoading, - childFragmentManager - ) { - MaterialProgressDialogFragment().also { dialog -> - dialog.message = getString(org.odk.collect.strings.R.string.loading) - } + viewModel.addLayers(layersDir) + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + MaterialProgressDialogFragment.showOn( + this, + viewModel.isLoading, + childFragmentManager + ) { + MaterialProgressDialogFragment().also { dialog -> + dialog.message = getString(org.odk.collect.strings.R.string.loading) } + } + + viewModel.isLoading.observe(this) { isLoading -> + binding.addLayerButton.isEnabled = !isLoading + } - isLoading.observe(this) { - if (!it) { - setFragmentResult(RESULT_KEY, bundleOf()) - dismiss() - } + viewModel.data.observe(this) { data -> + val adapter = OfflineMapLayersImporterAdapter(data) + binding.layers.setAdapter(adapter) + } + + viewModel.isAddingNewLayersFinished.observe(this) { isAddingNewLayersFinished -> + if (isAddingNewLayersFinished) { + setFragmentResult(RESULT_KEY, bundleOf()) + dismiss() } } - return binding.root } override fun onCloseClicked() { diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterViewModel.kt index 02f1c807643..4b297b40033 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterViewModel.kt @@ -16,6 +16,11 @@ class OfflineMapLayersImporterViewModel( private val scheduler: Scheduler, private val contentResolver: ContentResolver ) : ViewModel() { + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _isLoading + + private val _isAddingNewLayersFinished = MutableLiveData() + val isAddingNewLayersFinished: LiveData = _isAddingNewLayersFinished private val _data = MutableLiveData>() val data: LiveData> = _data @@ -23,6 +28,7 @@ class OfflineMapLayersImporterViewModel( private lateinit var tempLayersDir: File fun init(uris: ArrayList?) { + _isLoading.value = true scheduler.immediate( background = { tempLayersDir = TempFiles.createTempDir().also { @@ -40,14 +46,15 @@ class OfflineMapLayersImporterViewModel( } } } + _isLoading.postValue(false) _data.postValue(layers) }, foreground = { } ) } - fun addLayers(layersDir: String): LiveData { - val isLoading = MutableLiveData(true) + fun addLayers(layersDir: String) { + _isLoading.value = true scheduler.immediate( background = { val destDir = File(layersDir) @@ -56,10 +63,10 @@ class OfflineMapLayersImporterViewModel( } tempLayersDir.delete() - isLoading.postValue(false) + _isLoading.postValue(false) + _isAddingNewLayersFinished.postValue(true) }, foreground = { } ) - return isLoading } } diff --git a/maps/src/main/res/layout/offline_map_layers_importer.xml b/maps/src/main/res/layout/offline_map_layers_importer.xml index dbc456424e8..75cc50804eb 100644 --- a/maps/src/main/res/layout/offline_map_layers_importer.xml +++ b/maps/src/main/res/layout/offline_map_layers_importer.xml @@ -54,7 +54,6 @@ android:id="@+id/layers" android:layout_width="0dp" android:layout_height="wrap_content" - android:visibility="gone" android:layout_marginTop="@dimen/margin_small" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintStart_toStartOf="parent" @@ -62,24 +61,6 @@ app:layout_constraintTop_toBottomOf="@id/layers_title" tools:visibility="visible" /> - - - - diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt index b8564323800..914cd633e4e 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt @@ -15,6 +15,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.instanceOf import org.hamcrest.CoreMatchers.not import org.hamcrest.MatcherAssert.assertThat import org.junit.Rule @@ -22,6 +23,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule +import org.odk.collect.material.MaterialProgressDialogFragment import org.odk.collect.shared.TempFiles import org.odk.collect.strings.R import org.odk.collect.testshared.FakeScheduler @@ -46,6 +48,7 @@ class OfflineMapLayersImporterTest { @Test fun `clicking the 'cancel' button dismisses the dialog`() { launchFragment(arrayListOf()).onFragment { + scheduler.flush() assertThat(it.isVisible, equalTo(true)) onView(withText(R.string.cancel)).perform(click()) assertThat(it.isVisible, equalTo(false)) @@ -69,11 +72,15 @@ class OfflineMapLayersImporterTest { } @Test - fun `clicking the 'add layer' button dismisses the dialog`() { + fun `clicking the 'add layer' button displays progress indicator before adding layers and dismisses the dialog after`() { launchFragment(arrayListOf()).onFragment { scheduler.flush() assertThat(it.isVisible, equalTo(true)) onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) + assertThat( + it.childFragmentManager.findFragmentByTag(MaterialProgressDialogFragment::class.java.name), + instanceOf(MaterialProgressDialogFragment::class.java) + ) scheduler.flush() RobolectricHelpers.runLooper() assertThat(it.isVisible, equalTo(false)) @@ -104,15 +111,23 @@ class OfflineMapLayersImporterTest { val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) - launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) + val scenario = launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) - onView(withId(org.odk.collect.maps.R.id.progress_indicator)).check(matches(isDisplayed())) - onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(not(isDisplayed()))) + scenario.onFragment { + assertThat( + it.childFragmentManager.findFragmentByTag(MaterialProgressDialogFragment::class.java.name), + instanceOf(MaterialProgressDialogFragment::class.java) + ) + } scheduler.flush() - onView(withId(org.odk.collect.maps.R.id.progress_indicator)).check(matches(not(isDisplayed()))) - onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(isDisplayed())) + scenario.onFragment { + assertThat( + it.childFragmentManager.findFragmentByTag(MaterialProgressDialogFragment::class.java.name), + equalTo(null) + ) + } } @Test @@ -144,6 +159,8 @@ class OfflineMapLayersImporterTest { @Test fun `checking location sets selection correctly`() { launchFragment(arrayListOf()) + scheduler.flush() + onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(click()) onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(not(isChecked()))) @@ -158,6 +175,8 @@ class OfflineMapLayersImporterTest { @Test fun `recreating maintains the selected layers location`() { val scenario = launchFragment(arrayListOf()) + scheduler.flush() + onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(click()) scenario.recreate() From 2b49ef9db0e926d7444e51ef496c466dc85809da Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 5 Jun 2024 15:00:09 +0200 Subject: [PATCH 551/750] Fixed observations in OfflineMapLayersPicker --- .../maps/layers/OfflineMapLayersPicker.kt | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 0a004c2a8ac..b0e1c5849c0 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -77,23 +77,6 @@ class OfflineMapLayersPicker( ): View { offlineMapLayersPickerBinding = OfflineMapLayersPickerBinding.inflate(inflater) - viewModel.data.observe(this) { data -> - if (data == null) { - offlineMapLayersPickerBinding.progressIndicator.visibility = View.VISIBLE - offlineMapLayersPickerBinding.layers.visibility = View.GONE - offlineMapLayersPickerBinding.save.isEnabled = false - } else { - offlineMapLayersPickerBinding.progressIndicator.visibility = View.GONE - offlineMapLayersPickerBinding.layers.visibility = View.VISIBLE - offlineMapLayersPickerBinding.save.isEnabled = true - - val adapter = OfflineMapLayersPickerAdapter(data.first, data.second) { - viewModel.changeSelectedLayerId(it) - } - offlineMapLayersPickerBinding.layers.setAdapter(adapter) - } - } - offlineMapLayersPickerBinding.mbtilesInfoGroup.addOnClickListener { externalWebPageHelper.openWebPageInCustomTab( requireActivity(), @@ -117,6 +100,27 @@ class OfflineMapLayersPicker( return offlineMapLayersPickerBinding.root } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.data.observe(this) { data -> + if (data == null) { + offlineMapLayersPickerBinding.progressIndicator.visibility = View.VISIBLE + offlineMapLayersPickerBinding.layers.visibility = View.GONE + offlineMapLayersPickerBinding.save.isEnabled = false + } else { + offlineMapLayersPickerBinding.progressIndicator.visibility = View.GONE + offlineMapLayersPickerBinding.layers.visibility = View.VISIBLE + offlineMapLayersPickerBinding.save.isEnabled = true + + val adapter = OfflineMapLayersPickerAdapter(data.first, data.second) { + viewModel.changeSelectedLayerId(it) + } + offlineMapLayersPickerBinding.layers.setAdapter(adapter) + } + } + } + override fun onStart() { super.onStart() try { From c7f0236c5322fa08cbb403de41faaa49d3fb5e2c Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 5 Jun 2024 15:26:22 +0100 Subject: [PATCH 552/750] Remove ingore on test --- .../collect/android/feature/formentry/FieldListUpdateTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java index 02f706d575a..8f2c9ff752a 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java @@ -57,7 +57,6 @@ import androidx.test.espresso.matcher.ViewMatchers; import org.hamcrest.Matcher; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.RuleChain; @@ -393,7 +392,6 @@ public void searchMinimalInFieldList() { } @Test - @Ignore("https://github.com/getodk/collect/issues/5996") public void listOfQuestionsShouldNotBeScrolledToTheLastEditedQuestionAfterClickingOnAQuestion() { new FormEntryPage("fieldlist-updates") .clickGoToArrow() From 40148aa58f678fa6a7e8f47b4d871419a616f66d Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 5 Jun 2024 16:06:43 +0100 Subject: [PATCH 553/750] Remove broken test method --- .../feature/formentry/FieldListUpdateTest.java | 4 ++-- .../android/regression/FillBlankFormTest.java | 2 +- .../android/regression/SignatureWidgetTest.java | 2 +- .../org/odk/collect/android/support/pages/Page.kt | 12 ------------ 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java index 8f2c9ff752a..2658805c5de 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java @@ -400,7 +400,7 @@ public void listOfQuestionsShouldNotBeScrolledToTheLastEditedQuestionAfterClicki .clickOnQuestion("Question1") .answerQuestion(0, "X") .activateTextQuestion(19) - .checkIsTranslationDisplayed("Question20"); + .assertText("Question20"); } @Test @@ -413,7 +413,7 @@ public void recordingAudio_ShouldChangeRelevanceOfRelatedField() { .assertTextDoesNotExist("Target16") .clickOnString(org.odk.collect.strings.R.string.capture_audio) .clickOnContentDescription(org.odk.collect.strings.R.string.stop_recording) - .checkIsTranslationDisplayed("Target16") + .assertText("Target16") .clickOnString(org.odk.collect.strings.R.string.delete_answer_file) .clickOnTextInDialog(org.odk.collect.strings.R.string.delete_answer_file, new FormEntryPage("fieldlist-updates")) .assertTextDoesNotExist("Target16"); diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java index a0a6bb76ef2..23bd743b35f 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java @@ -97,7 +97,7 @@ public void formsWithDate_ShouldSaveFormsWithSuccess() { rule.startAtMainMenu() .copyForm("1560_DateData.xml") .startBlankForm("1560_DateData") - .checkIsTranslationDisplayed("Jan 01, 1900", "01 ene. 1900") + .assertText("Jan 01, 1900") .swipeToEndScreen("01/01/00") .clickFinalize() diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/regression/SignatureWidgetTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/regression/SignatureWidgetTest.java index 334b023698e..381cc738bd8 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/regression/SignatureWidgetTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/regression/SignatureWidgetTest.java @@ -35,7 +35,7 @@ public void saveIgnoreDialog_ShouldUseBothOptions() { .clickOnId(R.id.sign_button) .waitForRotationToEnd() .pressBack(new SaveOrIgnoreDrawingDialog<>("Gather Signature", new FormEntryPage("All widgets"))) - .checkIsTranslationDisplayed("Exit Gather Signature", "Salir Adjuntar firma") + .assertText("Exit Gather Signature") .assertText(org.odk.collect.strings.R.string.keep_changes) .clickDiscardChanges() .waitForRotationToEnd() diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt index 209a0d47038..5869eb1b918 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt @@ -10,7 +10,6 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView import androidx.test.espresso.NoActivityResumedException -import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.ViewAction import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.longClick @@ -169,17 +168,6 @@ abstract class Page> { return this as T } - fun checkIsTranslationDisplayed(vararg text: String?): T { - for (s in text) { - try { - onView(withText(s)).check(matches(isDisplayed())) - } catch (e: NoMatchingViewException) { - Timber.i(e) - } - } - return this as T - } - fun closeSoftKeyboard(): T { Espresso.closeSoftKeyboard() return this as T From bb1b3cadb8a5cb5386ce4d6f304a368a96ca8fb6 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 5 Jun 2024 17:43:05 +0100 Subject: [PATCH 554/750] Deprecate dangerous matcher --- .../odk/collect/android/support/matchers/CustomMatchers.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.java index 01b669dd018..a944b3db854 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.java @@ -33,7 +33,10 @@ private CustomMatchers() { /** * Matches the view at the given index. Useful when several views have the same properties. * https://stackoverflow.com/a/39756832 + * + * @deprecated this matcher is stateful and will cause problems if used more than once */ + @Deprecated public static Matcher withIndex(final Matcher matcher, final int index) { return new TypeSafeMatcher() { int currentIndex; From 8da1c7f2ffea4f1d810d8b58df7e6f8b60166a86 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 5 Jun 2024 22:02:29 +0200 Subject: [PATCH 555/750] Use one shared viewmodel --- .../maps/layers/OfflineMapLayersImporter.kt | 57 +++------ .../maps/layers/OfflineMapLayersPicker.kt | 61 ++++----- .../layers/OfflineMapLayersPickerViewModel.kt | 44 ------- ...wModel.kt => OfflineMapLayersViewModel.kt} | 58 +++++++-- .../layout/offline_map_layers_importer.xml | 25 +++- .../res/layout/offline_map_layers_picker.xml | 5 +- .../layers/OfflineMapLayersImporterTest.kt | 121 +++++++----------- .../maps/layers/OfflineMapLayersPickerTest.kt | 89 +++++++------ 8 files changed, 207 insertions(+), 253 deletions(-) delete mode 100644 maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt rename maps/src/main/java/org/odk/collect/maps/layers/{OfflineMapLayersImporterViewModel.kt => OfflineMapLayersViewModel.kt} (53%) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt index ec598ee1432..bef347ec9e6 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt @@ -5,26 +5,25 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.Toolbar -import androidx.core.os.bundleOf -import androidx.fragment.app.setFragmentResult -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import org.odk.collect.async.Scheduler import org.odk.collect.maps.databinding.OfflineMapLayersImporterBinding import org.odk.collect.material.MaterialFullScreenDialogFragment -import org.odk.collect.material.MaterialProgressDialogFragment +import org.odk.collect.settings.SettingsProvider class OfflineMapLayersImporter( + private val referenceLayerRepository: ReferenceLayerRepository, private val scheduler: Scheduler, + private val settingsProvider: SettingsProvider, private val sharedLayersDirPath: String, private val projectLayersDirPath: String ) : MaterialFullScreenDialogFragment() { - - private val viewModel: OfflineMapLayersImporterViewModel by viewModels { + val viewModel: OfflineMapLayersViewModel by activityViewModels { object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return OfflineMapLayersImporterViewModel(scheduler, requireContext().contentResolver) as T + return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider, requireContext().contentResolver) as T } } } @@ -38,8 +37,6 @@ class OfflineMapLayersImporter( ): View { binding = OfflineMapLayersImporterBinding.inflate(inflater) - viewModel.init(requireArguments().getStringArrayList(URIS)) - binding.cancelButton.setOnClickListener { dismiss() } @@ -51,7 +48,8 @@ class OfflineMapLayersImporter( projectLayersDirPath } - viewModel.addLayers(layersDir) + viewModel.importNewLayers(layersDir) + dismiss() } return binding.root } @@ -59,35 +57,25 @@ class OfflineMapLayersImporter( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - MaterialProgressDialogFragment.showOn( - this, - viewModel.isLoading, - childFragmentManager - ) { - MaterialProgressDialogFragment().also { dialog -> - dialog.message = getString(org.odk.collect.strings.R.string.loading) - } - } - viewModel.isLoading.observe(this) { isLoading -> - binding.addLayerButton.isEnabled = !isLoading + if (isLoading) { + binding.addLayerButton.isEnabled = false + binding.layers.visibility = View.GONE + binding.progressIndicator.visibility = View.VISIBLE + } else { + binding.addLayerButton.isEnabled = true + binding.layers.visibility = View.VISIBLE + binding.progressIndicator.visibility = View.GONE + } } - viewModel.data.observe(this) { data -> - val adapter = OfflineMapLayersImporterAdapter(data) + viewModel.layersToImport.observe(this) { layersToImport -> + val adapter = OfflineMapLayersImporterAdapter(layersToImport) binding.layers.setAdapter(adapter) } - - viewModel.isAddingNewLayersFinished.observe(this) { isAddingNewLayersFinished -> - if (isAddingNewLayersFinished) { - setFragmentResult(RESULT_KEY, bundleOf()) - dismiss() - } - } } - override fun onCloseClicked() { - } + override fun onCloseClicked() = Unit override fun onBackPressed() { dismiss() @@ -96,9 +84,4 @@ class OfflineMapLayersImporter( override fun getToolbar(): Toolbar { return binding.toolbar } - - companion object { - const val URIS = "uris" - const val RESULT_KEY = "layersAdded" - } } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index b0e1c5849c0..ed7439bf144 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -7,7 +7,7 @@ import android.view.View import android.view.ViewGroup import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.contract.ActivityResultContracts -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -27,26 +27,21 @@ class OfflineMapLayersPicker( private val settingsProvider: SettingsProvider, private val externalWebPageHelper: ExternalWebPageHelper ) : BottomSheetDialogFragment() { - private val viewModel: OfflineMapLayersPickerViewModel by viewModels { + private val viewModel: OfflineMapLayersViewModel by activityViewModels { object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return OfflineMapLayersPickerViewModel(referenceLayerRepository, scheduler, settingsProvider) as T + return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider, requireContext().contentResolver) as T } } } - private lateinit var offlineMapLayersPickerBinding: OfflineMapLayersPickerBinding + private lateinit var binding: OfflineMapLayersPickerBinding private val getLayers = registerForActivityResult(ActivityResultContracts.GetMultipleContents(), registry) { uris -> if (uris.isNotEmpty()) { - val uriStrings: MutableList = ArrayList() - for (uri in uris) { - uriStrings.add(uri.toString()) - } - + viewModel.loadLayersToImport(uris) DialogFragmentUtils.showIfNotShowing( OfflineMapLayersImporter::class.java, - Bundle().apply { putStringArrayList(OfflineMapLayersImporter.URIS, ArrayList(uriStrings)) }, childFragmentManager ) } @@ -56,7 +51,9 @@ class OfflineMapLayersPicker( childFragmentManager.fragmentFactory = FragmentFactoryBuilder() .forClass(OfflineMapLayersImporter::class) { OfflineMapLayersImporter( + referenceLayerRepository, scheduler, + settingsProvider, referenceLayerRepository.getSharedLayersDirPath(), referenceLayerRepository.getProjectLayersDirPath() ) @@ -64,10 +61,6 @@ class OfflineMapLayersPicker( .build() super.onCreate(savedInstanceState) - - childFragmentManager.setFragmentResultListener(OfflineMapLayersImporter.RESULT_KEY, this) { _, _ -> - viewModel.refreshLayers() - } } override fun onCreateView( @@ -75,49 +68,51 @@ class OfflineMapLayersPicker( container: ViewGroup?, savedInstanceState: Bundle? ): View { - offlineMapLayersPickerBinding = OfflineMapLayersPickerBinding.inflate(inflater) + binding = OfflineMapLayersPickerBinding.inflate(inflater) - offlineMapLayersPickerBinding.mbtilesInfoGroup.addOnClickListener { + binding.mbtilesInfoGroup.addOnClickListener { externalWebPageHelper.openWebPageInCustomTab( requireActivity(), Uri.parse("https://docs.getodk.org/collect-offline-maps/#transferring-offline-tilesets-to-devices") ) } - offlineMapLayersPickerBinding.addLayer.setOnClickListener { + binding.addLayer.setOnClickListener { getLayers.launch("*/*") } - offlineMapLayersPickerBinding.cancel.setOnClickListener { + binding.cancel.setOnClickListener { dismiss() } - offlineMapLayersPickerBinding.save.setOnClickListener { + binding.save.setOnClickListener { viewModel.saveSelectedLayer() dismiss() } - return offlineMapLayersPickerBinding.root + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel.data.observe(this) { data -> - if (data == null) { - offlineMapLayersPickerBinding.progressIndicator.visibility = View.VISIBLE - offlineMapLayersPickerBinding.layers.visibility = View.GONE - offlineMapLayersPickerBinding.save.isEnabled = false + viewModel.isLoading.observe(this) { isLoading -> + if (isLoading) { + binding.progressIndicator.visibility = View.VISIBLE + binding.layers.visibility = View.GONE + binding.save.isEnabled = false } else { - offlineMapLayersPickerBinding.progressIndicator.visibility = View.GONE - offlineMapLayersPickerBinding.layers.visibility = View.VISIBLE - offlineMapLayersPickerBinding.save.isEnabled = true - - val adapter = OfflineMapLayersPickerAdapter(data.first, data.second) { - viewModel.changeSelectedLayerId(it) - } - offlineMapLayersPickerBinding.layers.setAdapter(adapter) + binding.progressIndicator.visibility = View.GONE + binding.layers.visibility = View.VISIBLE + binding.save.isEnabled = true + } + } + + viewModel.existingLayers.observe(this) { layers -> + val adapter = OfflineMapLayersPickerAdapter(layers.first, layers.second) { + viewModel.changeSelectedLayerId(it) } + binding.layers.setAdapter(adapter) } } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt deleted file mode 100644 index 65ee45d63cf..00000000000 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt +++ /dev/null @@ -1,44 +0,0 @@ -package org.odk.collect.maps.layers - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.odk.collect.async.Scheduler -import org.odk.collect.settings.SettingsProvider -import org.odk.collect.settings.keys.ProjectKeys - -class OfflineMapLayersPickerViewModel( - private val referenceLayerRepository: ReferenceLayerRepository, - private val scheduler: Scheduler, - private val settingsProvider: SettingsProvider -) : ViewModel() { - private val _data = MutableLiveData, String?>?>(null) - val data: LiveData, String?>?> = _data - - init { - refreshLayers() - } - - fun saveSelectedLayer() { - val selectedLayerId = data.value?.second - settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, selectedLayerId) - } - - fun changeSelectedLayerId(selectedLayerId: String?) { - _data.postValue(_data.value?.copy(second = selectedLayerId)) - } - - fun refreshLayers() { - _data.value = null - - scheduler.immediate( - background = { - val layers = referenceLayerRepository.getAll() - val selectedLayerId = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) - - _data.postValue(Pair(layers, selectedLayerId)) - }, - foreground = { } - ) - } -} diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt similarity index 53% rename from maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterViewModel.kt rename to maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index 4b297b40033..0b632059048 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -8,26 +8,48 @@ import androidx.lifecycle.ViewModel import org.odk.collect.androidshared.system.getFileName import org.odk.collect.androidshared.system.toFile import org.odk.collect.async.Scheduler +import org.odk.collect.settings.SettingsProvider +import org.odk.collect.settings.keys.ProjectKeys import org.odk.collect.shared.TempFiles import java.io.File -import java.util.ArrayList -class OfflineMapLayersImporterViewModel( +class OfflineMapLayersViewModel( + private val referenceLayerRepository: ReferenceLayerRepository, private val scheduler: Scheduler, + private val settingsProvider: SettingsProvider, private val contentResolver: ContentResolver ) : ViewModel() { private val _isLoading = MutableLiveData() val isLoading: LiveData = _isLoading - private val _isAddingNewLayersFinished = MutableLiveData() - val isAddingNewLayersFinished: LiveData = _isAddingNewLayersFinished + private val _existingLayers = MutableLiveData, String?>>() + val existingLayers: LiveData, String?>> = _existingLayers - private val _data = MutableLiveData>() - val data: LiveData> = _data + private val _layersToImport = MutableLiveData>() + val layersToImport: LiveData> = _layersToImport private lateinit var tempLayersDir: File - fun init(uris: ArrayList?) { + init { + loadExistingLayers() + } + + private fun loadExistingLayers() { + _isLoading.value = true + scheduler.immediate( + background = { + val layers = referenceLayerRepository.getAll() + val selectedLayerId = + settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) + + _isLoading.postValue(false) + _existingLayers.postValue(Pair(layers, selectedLayerId)) + }, + foreground = { } + ) + } + + fun loadLayersToImport(uris: List) { _isLoading.value = true scheduler.immediate( background = { @@ -35,8 +57,7 @@ class OfflineMapLayersImporterViewModel( it.deleteOnExit() } val layers = mutableListOf() - uris?.forEach { uriString -> - val uri = Uri.parse(uriString) + uris.forEach { uri -> uri.getFileName(contentResolver)?.let { fileName -> if (fileName.endsWith(MbtilesFile.FILE_EXTENSION)) { val layerFile = File(tempLayersDir, fileName).also { file -> @@ -47,13 +68,13 @@ class OfflineMapLayersImporterViewModel( } } _isLoading.postValue(false) - _data.postValue(layers) + _layersToImport.postValue(layers) }, foreground = { } ) } - fun addLayers(layersDir: String) { + fun importNewLayers(layersDir: String) { _isLoading.value = true scheduler.immediate( background = { @@ -62,11 +83,20 @@ class OfflineMapLayersImporterViewModel( it.copyTo(File(destDir, it.name), true) } tempLayersDir.delete() - _isLoading.postValue(false) - _isAddingNewLayersFinished.postValue(true) }, - foreground = { } + foreground = { + loadExistingLayers() + } ) } + + fun saveSelectedLayer() { + val selectedLayerId = existingLayers.value?.second + settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, selectedLayerId) + } + + fun changeSelectedLayerId(selectedLayerId: String?) { + _existingLayers.postValue(_existingLayers.value?.copy(second = selectedLayerId)) + } } diff --git a/maps/src/main/res/layout/offline_map_layers_importer.xml b/maps/src/main/res/layout/offline_map_layers_importer.xml index 75cc50804eb..2b678de5f32 100644 --- a/maps/src/main/res/layout/offline_map_layers_importer.xml +++ b/maps/src/main/res/layout/offline_map_layers_importer.xml @@ -1,7 +1,6 @@ @@ -58,8 +57,25 @@ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@id/layers_title" - tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@id/layers_title" /> + + + + @@ -141,7 +157,6 @@ android:layout_marginEnd="@dimen/margin_standard" android:layout_marginBottom="@dimen/margin_extra_small" android:text="@string/add_layer" - android:enabled="false" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> diff --git a/maps/src/main/res/layout/offline_map_layers_picker.xml b/maps/src/main/res/layout/offline_map_layers_picker.xml index b6c5d778549..5e86a2eeebe 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker.xml @@ -1,7 +1,6 @@ @@ -90,14 +89,12 @@ android:id="@+id/layers" android:layout_width="0dp" android:layout_height="0dp" - android:visibility="gone" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toTopOf="@id/add_layer" app:layout_constraintHeight_default="wrap" - app:layout_constraintTop_toBottomOf="@id/top_divider" - tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@id/top_divider" /> ().also { + whenever(it.getAll()).thenReturn(emptyList()) + whenever(it.getSharedLayersDirPath()).thenReturn(sharedLayersDirPath) + whenever(it.getProjectLayersDirPath()).thenReturn(projectLayersDirPath) + } + private val settingsProvider = InMemSettingsProvider() @get:Rule val fragmentScenarioLauncherRule = FragmentScenarioLauncherRule( FragmentFactoryBuilder() .forClass(OfflineMapLayersImporter::class) { - OfflineMapLayersImporter(scheduler, sharedLayersDirPath, projectLayersDirPath) + OfflineMapLayersImporter(referenceLayerRepository, scheduler, settingsProvider, sharedLayersDirPath, projectLayersDirPath) }.build() ) @Test fun `clicking the 'cancel' button dismisses the dialog`() { - launchFragment(arrayListOf()).onFragment { + launchFragment().onFragment { scheduler.flush() assertThat(it.isVisible, equalTo(true)) onView(withText(R.string.cancel)).perform(click()) @@ -56,83 +62,39 @@ class OfflineMapLayersImporterTest { } @Test - fun `clicking the 'cancel' button does not set fragment result`() { - launchFragment(arrayListOf()).onFragment { - var resultReceived = false - it.parentFragmentManager.setFragmentResultListener( - OfflineMapLayersImporter.RESULT_KEY, - it - ) { _, _ -> - resultReceived = true - } - - onView(withId(org.odk.collect.maps.R.id.cancel_button)).perform(click()) - assertThat(resultReceived, equalTo(false)) - } - } - - @Test - fun `clicking the 'add layer' button displays progress indicator before adding layers and dismisses the dialog after`() { - launchFragment(arrayListOf()).onFragment { + fun `clicking the 'add layer' button dismisses the dialog`() { + launchFragment().onFragment { scheduler.flush() assertThat(it.isVisible, equalTo(true)) + it.viewModel.loadLayersToImport(emptyList()) onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) - assertThat( - it.childFragmentManager.findFragmentByTag(MaterialProgressDialogFragment::class.java.name), - instanceOf(MaterialProgressDialogFragment::class.java) - ) scheduler.flush() RobolectricHelpers.runLooper() assertThat(it.isVisible, equalTo(false)) } } - @Test - fun `clicking the 'add layer' button sets fragment result`() { - launchFragment(arrayListOf()).onFragment { - scheduler.flush() - var resultReceived = false - it.parentFragmentManager.setFragmentResultListener( - OfflineMapLayersImporter.RESULT_KEY, - it - ) { _, _ -> - resultReceived = true - } - - onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) - scheduler.flush() - RobolectricHelpers.runLooper() - assertThat(resultReceived, equalTo(true)) - } - } - @Test fun `progress indicator is displayed during loading layers`() { val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) - val scenario = launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) - - scenario.onFragment { - assertThat( - it.childFragmentManager.findFragmentByTag(MaterialProgressDialogFragment::class.java.name), - instanceOf(MaterialProgressDialogFragment::class.java) - ) + launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri())) } + onView(withId(org.odk.collect.maps.R.id.progress_indicator)).check(matches(isDisplayed())) + onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(not(isDisplayed()))) + scheduler.flush() - scenario.onFragment { - assertThat( - it.childFragmentManager.findFragmentByTag(MaterialProgressDialogFragment::class.java.name), - equalTo(null) - ) - } + onView(withId(org.odk.collect.maps.R.id.progress_indicator)).check(matches(not(isDisplayed()))) + onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(isDisplayed())) } @Test fun `the 'cancel' button is enabled during loading layers`() { - launchFragment(arrayListOf()) + launchFragment() onView(withId(org.odk.collect.maps.R.id.cancel_button)).check(matches(isEnabled())) scheduler.flush() @@ -141,7 +103,7 @@ class OfflineMapLayersImporterTest { @Test fun `the 'add layer' button is disabled during loading layers`() { - launchFragment(arrayListOf()) + launchFragment() onView(withId(org.odk.collect.maps.R.id.add_layer_button)).check(matches(not(isEnabled()))) scheduler.flush() @@ -150,7 +112,7 @@ class OfflineMapLayersImporterTest { @Test fun `'All projects' location should be selected by default`() { - launchFragment(arrayListOf()) + launchFragment() onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(isChecked())) onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(not(isChecked()))) @@ -158,7 +120,7 @@ class OfflineMapLayersImporterTest { @Test fun `checking location sets selection correctly`() { - launchFragment(arrayListOf()) + launchFragment() scheduler.flush() onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(click()) @@ -174,7 +136,7 @@ class OfflineMapLayersImporterTest { @Test fun `recreating maintains the selected layers location`() { - val scenario = launchFragment(arrayListOf()) + val scenario = launchFragment() scheduler.flush() onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(click()) @@ -190,7 +152,9 @@ class OfflineMapLayersImporterTest { val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) - launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) + launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri())) + } scheduler.flush() @@ -204,7 +168,10 @@ class OfflineMapLayersImporterTest { val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) - val scenario = launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) + val scenario = launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri())) + } + scheduler.flush() scenario.recreate() @@ -219,7 +186,10 @@ class OfflineMapLayersImporterTest { val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) val file2 = TempFiles.createTempFile("layer2", ".txt") - launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) + launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri())) + } + scheduler.flush() onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(1))) @@ -236,7 +206,10 @@ class OfflineMapLayersImporterTest { it.writeText("blah2") } - launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) + launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri())) + } + scheduler.flush() onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) @@ -263,7 +236,10 @@ class OfflineMapLayersImporterTest { it.writeText("blah2") } - launchFragment(arrayListOf(file1.toUri().toString(), file2.toUri().toString())) + launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri())) + } + scheduler.flush() onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(scrollTo(), click()) @@ -283,10 +259,7 @@ class OfflineMapLayersImporterTest { assertThat(copiedFile2.readText(), equalTo("blah2")) } - private fun launchFragment(uris: ArrayList): FragmentScenario { - return fragmentScenarioLauncherRule.launchInContainer( - OfflineMapLayersImporter::class.java, - Bundle().apply { putStringArrayList(OfflineMapLayersImporter.URIS, uris) } - ) + private fun launchFragment(): FragmentScenario { + return fragmentScenarioLauncherRule.launchInContainer(OfflineMapLayersImporter::class.java) } } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index 7d523a15226..2c3e0cdebb4 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -5,11 +5,12 @@ import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts import androidx.core.app.ActivityOptionsCompat -import androidx.core.os.bundleOf +import androidx.core.net.toUri import androidx.fragment.app.testing.FragmentScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed @@ -45,8 +46,8 @@ import org.odk.collect.webpage.ExternalWebPageHelper class OfflineMapLayersPickerTest { private val referenceLayerRepository = mock().also { whenever(it.getAll()).thenReturn(emptyList()) - whenever(it.getSharedLayersDirPath()).thenReturn("") - whenever(it.getProjectLayersDirPath()).thenReturn("") + whenever(it.getSharedLayersDirPath()).thenReturn(TempFiles.createTempDir().absolutePath) + whenever(it.getProjectLayersDirPath()).thenReturn(TempFiles.createTempDir().absolutePath) } private val scheduler = FakeScheduler() private val settingsProvider = InMemSettingsProvider() @@ -76,7 +77,7 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'cancel' button dismisses the layers picker`() { - val scenario = launchFragment() + val scenario = launchOfflineMapLayersPicker() scenario.onFragment { assertThat(it.isVisible, equalTo(true)) @@ -91,7 +92,7 @@ class OfflineMapLayersPickerTest { ReferenceLayer("1", TempFiles.createTempFile(), "layer1") )) - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -101,14 +102,14 @@ class OfflineMapLayersPickerTest { @Test fun `the 'cancel' button should be enabled during loading layers`() { - launchFragment() + launchOfflineMapLayersPicker() onView(withText(string.cancel)).check(matches(isEnabled())) } @Test fun `clicking the 'save' button dismisses the layers picker`() { - val scenario = launchFragment() + val scenario = launchOfflineMapLayersPicker() scheduler.flush() @@ -121,7 +122,7 @@ class OfflineMapLayersPickerTest { @Test fun `the 'save' button should be disabled during loading layers`() { - launchFragment() + launchOfflineMapLayersPicker() onView(withText(string.save)).check(matches(not(isEnabled()))) scheduler.flush() @@ -134,7 +135,7 @@ class OfflineMapLayersPickerTest { ReferenceLayer("1", TempFiles.createTempFile(), "layer1") )) - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -148,7 +149,7 @@ class OfflineMapLayersPickerTest { ReferenceLayer("1", TempFiles.createTempFile(), "layer1") )) - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -163,7 +164,7 @@ class OfflineMapLayersPickerTest { ReferenceLayer("1", TempFiles.createTempFile(), "layer1") )) - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -179,7 +180,7 @@ class OfflineMapLayersPickerTest { )) settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, "2") - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -190,7 +191,7 @@ class OfflineMapLayersPickerTest { @Test fun `progress indicator is displayed during loading layers`() { - launchFragment() + launchOfflineMapLayersPicker() onView(withId(R.id.progress_indicator)).check(matches(isDisplayed())) onView(withId(R.id.layers)).check(matches(not(isDisplayed()))) @@ -203,14 +204,14 @@ class OfflineMapLayersPickerTest { @Test fun `the 'learn more' button should be enabled during loading layers`() { - launchFragment() + launchOfflineMapLayersPicker() onView(withText(string.get_help_with_reference_layers)).check(matches(isEnabled())) } @Test fun `clicking the 'learn more' button opens the forum thread`() { - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -221,7 +222,7 @@ class OfflineMapLayersPickerTest { @Test fun `if there are no layers the 'none' option is displayed`() { - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -236,7 +237,7 @@ class OfflineMapLayersPickerTest { ReferenceLayer("2", TempFiles.createTempFile(), "layer2") )) - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -252,7 +253,7 @@ class OfflineMapLayersPickerTest { ReferenceLayer("1", TempFiles.createTempFile(), "layer1") )) - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -271,7 +272,7 @@ class OfflineMapLayersPickerTest { ReferenceLayer("1", TempFiles.createTempFile(), "layer1") )) - val scenario = launchFragment() + val scenario = launchOfflineMapLayersPicker() scheduler.flush() @@ -283,7 +284,7 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'add layer' and selecting layers displays the confirmation dialog`() { - val scenario = launchFragment() + val scenario = launchOfflineMapLayersPicker() uris.add(Uri.parse("blah")) onView(withText(string.add_layer)).perform(click()) @@ -298,7 +299,7 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'add layer' and selecting nothing does not display the confirmation dialog`() { - val scenario = launchFragment() + val scenario = launchOfflineMapLayersPicker() onView(withText(string.add_layer)).perform(click()) @@ -312,53 +313,57 @@ class OfflineMapLayersPickerTest { @Test fun `progress indicator is displayed during loading layers after receiving new ones`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) - val scenario = launchFragment() + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) + + launchOfflineMapLayersPicker() scheduler.flush() - scenario.onFragment { - it.childFragmentManager.setFragmentResult(OfflineMapLayersImporter.RESULT_KEY, bundleOf()) - } + uris.add(file1.toUri()) + uris.add(file2.toUri()) + + onView(withText(string.add_layer)).perform(click()) + scheduler.flush() + onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(click()) onView(withId(R.id.progress_indicator)).check(matches(isDisplayed())) onView(withId(R.id.layers)).check(matches(not(isDisplayed()))) scheduler.flush() + onView(withId(R.id.progress_indicator)).check(matches(not(isDisplayed()))) onView(withId(R.id.layers)).check(matches(isDisplayed())) } @Test fun `when new layers added the list should be updated`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) - val scenario = launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() + uris.add(file1.toUri()) + uris.add(file2.toUri()) + + onView(withText(string.add_layer)).perform(click()) + scheduler.flush() + onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(click()) whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), - ReferenceLayer("2", TempFiles.createTempFile(), "layer2") + ReferenceLayer("1", TempFiles.createTempFile(), file1.name), + ReferenceLayer("1", TempFiles.createTempFile(), file2.name) )) - - scenario.onFragment { - it.childFragmentManager.setFragmentResult(OfflineMapLayersImporter.RESULT_KEY, bundleOf()) - } - scheduler.flush() onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3))) onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(withText(string.none))) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(withText("layer1"))) - onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.radio_button)).check(matches(withText("layer2"))) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(withText(file1.name))) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.radio_button)).check(matches(withText(file2.name))) } - private fun launchFragment(): FragmentScenario { + private fun launchOfflineMapLayersPicker(): FragmentScenario { return fragmentScenarioLauncherRule.launchInContainer(OfflineMapLayersPicker::class.java) } } From 3cb509accae85fcb1a9afd6ffc6833e5a2366640 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 5 Jun 2024 22:58:49 +0200 Subject: [PATCH 556/750] Improved tests --- .../maps/layers/OfflineMapLayersImporter.kt | 8 +-- .../maps/layers/OfflineMapLayersPicker.kt | 8 +-- .../layers/OfflineMapLayersImporterTest.kt | 31 ++++------ .../maps/layers/OfflineMapLayersPickerTest.kt | 58 +++++++++---------- .../layers/TestReferenceLayerRepository.kt | 32 ++++++++++ 5 files changed, 75 insertions(+), 62 deletions(-) create mode 100644 maps/src/test/java/org/odk/collect/maps/layers/TestReferenceLayerRepository.kt diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt index bef347ec9e6..ce2ce693339 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt @@ -16,9 +16,7 @@ import org.odk.collect.settings.SettingsProvider class OfflineMapLayersImporter( private val referenceLayerRepository: ReferenceLayerRepository, private val scheduler: Scheduler, - private val settingsProvider: SettingsProvider, - private val sharedLayersDirPath: String, - private val projectLayersDirPath: String + private val settingsProvider: SettingsProvider ) : MaterialFullScreenDialogFragment() { val viewModel: OfflineMapLayersViewModel by activityViewModels { object : ViewModelProvider.Factory { @@ -43,9 +41,9 @@ class OfflineMapLayersImporter( binding.addLayerButton.setOnClickListener { val layersDir = if (binding.allProjectsOption.isChecked) { - sharedLayersDirPath + referenceLayerRepository.getSharedLayersDirPath() } else { - projectLayersDirPath + referenceLayerRepository.getProjectLayersDirPath() } viewModel.importNewLayers(layersDir) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index ed7439bf144..739e1c932df 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -50,13 +50,7 @@ class OfflineMapLayersPicker( override fun onCreate(savedInstanceState: Bundle?) { childFragmentManager.fragmentFactory = FragmentFactoryBuilder() .forClass(OfflineMapLayersImporter::class) { - OfflineMapLayersImporter( - referenceLayerRepository, - scheduler, - settingsProvider, - referenceLayerRepository.getSharedLayersDirPath(), - referenceLayerRepository.getProjectLayersDirPath() - ) + OfflineMapLayersImporter(referenceLayerRepository, scheduler, settingsProvider) } .build() diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt index 756503dcd35..a1ae2b54cfa 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt @@ -19,13 +19,12 @@ import org.hamcrest.MatcherAssert.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule import org.odk.collect.settings.InMemSettingsProvider import org.odk.collect.shared.TempFiles import org.odk.collect.strings.R +import org.odk.collect.testshared.EspressoHelpers import org.odk.collect.testshared.FakeScheduler import org.odk.collect.testshared.RecyclerViewMatcher import org.odk.collect.testshared.RobolectricHelpers @@ -34,20 +33,14 @@ import java.io.File @RunWith(AndroidJUnit4::class) class OfflineMapLayersImporterTest { private val scheduler = FakeScheduler() - private val sharedLayersDirPath = TempFiles.createTempDir().absolutePath - private val projectLayersDirPath = TempFiles.createTempDir().absolutePath - private val referenceLayerRepository = mock().also { - whenever(it.getAll()).thenReturn(emptyList()) - whenever(it.getSharedLayersDirPath()).thenReturn(sharedLayersDirPath) - whenever(it.getProjectLayersDirPath()).thenReturn(projectLayersDirPath) - } + private val referenceLayerRepository = TestReferenceLayerRepository() private val settingsProvider = InMemSettingsProvider() @get:Rule val fragmentScenarioLauncherRule = FragmentScenarioLauncherRule( FragmentFactoryBuilder() .forClass(OfflineMapLayersImporter::class) { - OfflineMapLayersImporter(referenceLayerRepository, scheduler, settingsProvider, sharedLayersDirPath, projectLayersDirPath) + OfflineMapLayersImporter(referenceLayerRepository, scheduler, settingsProvider) }.build() ) @@ -56,7 +49,7 @@ class OfflineMapLayersImporterTest { launchFragment().onFragment { scheduler.flush() assertThat(it.isVisible, equalTo(true)) - onView(withText(R.string.cancel)).perform(click()) + EspressoHelpers.clickOnText(R.string.cancel) assertThat(it.isVisible, equalTo(false)) } } @@ -215,14 +208,14 @@ class OfflineMapLayersImporterTest { onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) scheduler.flush() - assertThat(File(sharedLayersDirPath).listFiles().size, equalTo(2)) - assertThat(File(projectLayersDirPath).listFiles().size, equalTo(0)) + assertThat(File(referenceLayerRepository.getSharedLayersDirPath()).listFiles().size, equalTo(2)) + assertThat(File(referenceLayerRepository.getProjectLayersDirPath()).listFiles().size, equalTo(0)) - val copiedFile1 = File(sharedLayersDirPath, file1.name) + val copiedFile1 = File(referenceLayerRepository.getSharedLayersDirPath(), file1.name) assertThat(copiedFile1.exists(), equalTo(true)) assertThat(copiedFile1.readText(), equalTo("blah1")) - val copiedFile2 = File(sharedLayersDirPath, file2.name) + val copiedFile2 = File(referenceLayerRepository.getSharedLayersDirPath(), file2.name) assertThat(copiedFile2.exists(), equalTo(true)) assertThat(copiedFile2.readText(), equalTo("blah2")) } @@ -247,14 +240,14 @@ class OfflineMapLayersImporterTest { onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) scheduler.flush() - assertThat(File(sharedLayersDirPath).listFiles().size, equalTo(0)) - assertThat(File(projectLayersDirPath).listFiles().size, equalTo(2)) + assertThat(File(referenceLayerRepository.getSharedLayersDirPath()).listFiles().size, equalTo(0)) + assertThat(File(referenceLayerRepository.getProjectLayersDirPath()).listFiles().size, equalTo(2)) - val copiedFile1 = File(projectLayersDirPath, file1.name) + val copiedFile1 = File(referenceLayerRepository.getProjectLayersDirPath(), file1.name) assertThat(copiedFile1.exists(), equalTo(true)) assertThat(copiedFile1.readText(), equalTo("blah1")) - val copiedFile2 = File(projectLayersDirPath, file2.name) + val copiedFile2 = File(referenceLayerRepository.getProjectLayersDirPath(), file2.name) assertThat(copiedFile2.exists(), equalTo(true)) assertThat(copiedFile2.readText(), equalTo("blah2")) } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index 2c3e0cdebb4..655941a0fad 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -28,7 +28,6 @@ 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 org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule import org.odk.collect.maps.R @@ -44,11 +43,7 @@ import org.odk.collect.webpage.ExternalWebPageHelper @RunWith(AndroidJUnit4::class) class OfflineMapLayersPickerTest { - private val referenceLayerRepository = mock().also { - whenever(it.getAll()).thenReturn(emptyList()) - whenever(it.getSharedLayersDirPath()).thenReturn(TempFiles.createTempDir().absolutePath) - whenever(it.getProjectLayersDirPath()).thenReturn(TempFiles.createTempDir().absolutePath) - } + private val referenceLayerRepository = TestReferenceLayerRepository() private val scheduler = FakeScheduler() private val settingsProvider = InMemSettingsProvider() private val externalWebPageHelper = mock() @@ -88,9 +83,9 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'cancel' button does not save the layer`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + referenceLayerRepository.addLayers( ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) + ) launchOfflineMapLayersPicker() @@ -131,9 +126,9 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'save' button saves null when 'None' option is checked`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + referenceLayerRepository.addLayers( ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) + ) launchOfflineMapLayersPicker() @@ -145,9 +140,9 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'save' button saves the layer id if any is checked`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + referenceLayerRepository.addLayers( ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) + ) launchOfflineMapLayersPicker() @@ -160,9 +155,9 @@ class OfflineMapLayersPickerTest { @Test fun `when no layer id is saved in settings the 'None' option should be checked`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + referenceLayerRepository.addLayers( ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) + ) launchOfflineMapLayersPicker() @@ -174,10 +169,11 @@ class OfflineMapLayersPickerTest { @Test fun `when layer id is saved in settings the layer it belongs to should be checked`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + referenceLayerRepository.addLayers( ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), ReferenceLayer("2", TempFiles.createTempFile(), "layer2") - )) + ) + settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, "2") launchOfflineMapLayersPicker() @@ -215,7 +211,7 @@ class OfflineMapLayersPickerTest { scheduler.flush() - onView(withText(string.get_help_with_reference_layers)).perform(click()) + EspressoHelpers.clickOnText(string.get_help_with_reference_layers) verify(externalWebPageHelper).openWebPageInCustomTab(any(), eq(Uri.parse("https://docs.getodk.org/collect-offline-maps/#transferring-offline-tilesets-to-devices"))) } @@ -232,10 +228,10 @@ class OfflineMapLayersPickerTest { @Test fun `if there are multiple layers all of them are displayed along with the 'None'`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + referenceLayerRepository.addLayers( ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), ReferenceLayer("2", TempFiles.createTempFile(), "layer2") - )) + ) launchOfflineMapLayersPicker() @@ -249,9 +245,9 @@ class OfflineMapLayersPickerTest { @Test fun `checking layers sets selection correctly`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + referenceLayerRepository.addLayers( ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) + ) launchOfflineMapLayersPicker() @@ -268,9 +264,9 @@ class OfflineMapLayersPickerTest { @Test fun `recreating maintains selection`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + referenceLayerRepository.addLayers( ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) + ) val scenario = launchOfflineMapLayersPicker() @@ -287,7 +283,7 @@ class OfflineMapLayersPickerTest { val scenario = launchOfflineMapLayersPicker() uris.add(Uri.parse("blah")) - onView(withText(string.add_layer)).perform(click()) + EspressoHelpers.clickOnText(string.add_layer) scenario.onFragment { assertThat( @@ -301,7 +297,7 @@ class OfflineMapLayersPickerTest { fun `clicking the 'add layer' and selecting nothing does not display the confirmation dialog`() { val scenario = launchOfflineMapLayersPicker() - onView(withText(string.add_layer)).perform(click()) + EspressoHelpers.clickOnText(string.add_layer) scenario.onFragment { assertThat( @@ -323,7 +319,7 @@ class OfflineMapLayersPickerTest { uris.add(file1.toUri()) uris.add(file2.toUri()) - onView(withText(string.add_layer)).perform(click()) + EspressoHelpers.clickOnText(string.add_layer) scheduler.flush() onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(click()) @@ -348,13 +344,13 @@ class OfflineMapLayersPickerTest { uris.add(file1.toUri()) uris.add(file2.toUri()) - onView(withText(string.add_layer)).perform(click()) + EspressoHelpers.clickOnText(string.add_layer) scheduler.flush() onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(click()) - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), file1.name), - ReferenceLayer("1", TempFiles.createTempFile(), file2.name) - )) + referenceLayerRepository.addLayers( + ReferenceLayer("1", file1, file1.name), + ReferenceLayer("2", file2, file2.name) + ) scheduler.flush() onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3))) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/TestReferenceLayerRepository.kt b/maps/src/test/java/org/odk/collect/maps/layers/TestReferenceLayerRepository.kt new file mode 100644 index 00000000000..2226f0c854c --- /dev/null +++ b/maps/src/test/java/org/odk/collect/maps/layers/TestReferenceLayerRepository.kt @@ -0,0 +1,32 @@ +package org.odk.collect.maps.layers + +import org.odk.collect.shared.TempFiles + +class TestReferenceLayerRepository : ReferenceLayerRepository { + private val sharedLayersDirPath: String = TempFiles.createTempDir().absolutePath + private val projectLayersDirPath: String = TempFiles.createTempDir().absolutePath + + private val layers = mutableListOf() + + override fun getAll(): List { + return layers + } + + override fun get(id: String): ReferenceLayer? { + return layers.find { it.id == id } + } + + override fun getSharedLayersDirPath(): String { + return sharedLayersDirPath + } + + override fun getProjectLayersDirPath(): String { + return projectLayersDirPath + } + + fun addLayers(vararg newLayers: ReferenceLayer) { + newLayers.forEach { newLayer -> + layers.add(newLayer) + } + } +} From cee27e7fb03e0b5ad71f3bc32bae4346f1e842ba Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 6 Jun 2024 10:22:52 +0100 Subject: [PATCH 557/750] Convert custom matchers to Kotlin --- ...{CustomMatchers.java => CustomMatchers.kt} | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) rename collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/{CustomMatchers.java => CustomMatchers.kt} (51%) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.kt similarity index 51% rename from collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.java rename to collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.kt index a944b3db854..6b8a841d567 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.kt @@ -13,45 +13,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.odk.collect.android.support.matchers -package org.odk.collect.android.support.matchers; - -import android.view.View; - -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.TypeSafeMatcher; +import android.view.View +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.TypeSafeMatcher /** * Grab bag of Hamcrest matchers. */ -public final class CustomMatchers { - private CustomMatchers() { - - } - +object CustomMatchers { /** * Matches the view at the given index. Useful when several views have the same properties. * https://stackoverflow.com/a/39756832 * - * @deprecated this matcher is stateful and will cause problems if used more than once */ - @Deprecated - public static Matcher withIndex(final Matcher matcher, final int index) { - return new TypeSafeMatcher() { - int currentIndex; - - @Override - public void describeTo(Description description) { - description.appendText("with index: "); - description.appendValue(index); - matcher.describeTo(description); + @Deprecated("this matcher is stateful and will cause problems if used more than once") + @JvmStatic + fun withIndex(matcher: Matcher, index: Int): TypeSafeMatcher { + return object : TypeSafeMatcher() { + var currentIndex: Int = 0 + + override fun describeTo(description: Description) { + description.appendText("with index: ") + description.appendValue(index) + matcher.describeTo(description) } - @Override - public boolean matchesSafely(View view) { - return matcher.matches(view) && currentIndex++ == index; + override fun matchesSafely(view: View): Boolean { + return matcher.matches(view) && currentIndex++ == index } - }; + } } } From 8e6fe7a983e71d8914e1afa2f34ba1bb61995b51 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 6 Jun 2024 10:45:23 +0100 Subject: [PATCH 558/750] Create custom matcher for question widgets --- .../formentry/FieldListUpdateTest.java | 2 +- .../support/matchers/CustomMatchers.kt | 22 +++++++++++++++++++ .../android/support/pages/FormEntryPage.java | 8 +++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java index 2658805c5de..4399ed05961 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java @@ -399,7 +399,7 @@ public void listOfQuestionsShouldNotBeScrolledToTheLastEditedQuestionAfterClicki .clickOnGroup("Long list of questions") .clickOnQuestion("Question1") .answerQuestion(0, "X") - .activateTextQuestion(19) + .activateTextQuestion("Question20") .assertText("Question20"); } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.kt index 6b8a841d567..7380003cef4 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.kt @@ -16,9 +16,12 @@ package org.odk.collect.android.support.matchers import android.view.View +import android.widget.TextView import org.hamcrest.Description import org.hamcrest.Matcher import org.hamcrest.TypeSafeMatcher +import org.odk.collect.android.R +import org.odk.collect.android.widgets.QuestionWidget /** * Grab bag of Hamcrest matchers. @@ -46,4 +49,23 @@ object CustomMatchers { } } } + + @JvmStatic + fun isQuestionView(questionText: String): Matcher { + return object : TypeSafeMatcher() { + override fun describeTo(description: Description) { + description.appendText("is question view with text: ") + description.appendValue(questionText) + } + + override fun matchesSafely(item: View): Boolean { + return if (item is QuestionWidget) { + val questionTextView = item.findViewById(R.id.text_label) + questionTextView.text.toString() == questionText + } else { + false + } + } + } + } } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java index 89a9cf4e136..1f6781616b8 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java @@ -8,13 +8,16 @@ import static androidx.test.espresso.action.ViewActions.swipeLeft; import static androidx.test.espresso.action.ViewActions.swipeRight; import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withClassName; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.hamcrest.core.StringEndsWith.endsWith; +import static org.odk.collect.android.support.matchers.CustomMatchers.isQuestionView; import static org.odk.collect.android.support.matchers.CustomMatchers.withIndex; import android.os.Build; @@ -318,6 +321,11 @@ public FormEntryPage activateTextQuestion(int index) { return this; } + public FormEntryPage activateTextQuestion(String questionText) { + onView(allOf(withClassName(endsWith("EditText")), isDescendantOfA(isQuestionView(questionText)))).perform(scrollTo(), click()); + return this; + } + public FormEntryPage assertQuestion(String text) { return assertQuestion(text, false); } From 78a5f0a20960fc31e58f0e34e586baa2729accf9 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 6 Jun 2024 10:52:16 +0100 Subject: [PATCH 559/750] Use clickOn interaction --- .../feature/formentry/FieldListUpdateTest.java | 2 +- .../odk/collect/android/support/Interactions.kt | 2 ++ .../android/support/pages/FormEntryPage.java | 17 ++++++++--------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java index 4399ed05961..655a1ba3bad 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.java @@ -399,7 +399,7 @@ public void listOfQuestionsShouldNotBeScrolledToTheLastEditedQuestionAfterClicki .clickOnGroup("Long list of questions") .clickOnQuestion("Question1") .answerQuestion(0, "X") - .activateTextQuestion("Question20") + .clickOnQuestionField("Question20") .assertText("Question20"); } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt index af7609962b9..1f9c209191b 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt @@ -16,6 +16,8 @@ object Interactions { * initially clicking on the view fails, this will then attempt to scroll to the view and * retry the click. */ + @JvmStatic + @JvmOverloads fun clickOn(view: Matcher, root: Matcher? = null) { val onView = if (root != null) { onView(view).inRoot(root) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java index 1f6781616b8..af6150dbd60 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java @@ -21,11 +21,14 @@ import static org.odk.collect.android.support.matchers.CustomMatchers.withIndex; import android.os.Build; +import android.view.View; import androidx.test.core.app.ApplicationProvider; +import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.odk.collect.android.R; +import org.odk.collect.android.support.Interactions; import org.odk.collect.android.support.WaitFor; import java.util.concurrent.Callable; @@ -54,7 +57,7 @@ public FormEntryPage assertOnPage() { // Check we are not on the Form Hierarchy page assertTextDoesNotExist(org.odk.collect.strings.R.string.jump_to_beginning); assertTextDoesNotExist(org.odk.collect.strings.R.string.jump_to_end); - + return this; } @@ -315,14 +318,10 @@ public FormEntryPage answerQuestion(int index, String answer) { return this; } - public FormEntryPage activateTextQuestion(int index) { - onView(withIndex(withClassName(endsWith("EditText")), index)).perform(scrollTo()); - onView(withIndex(withClassName(endsWith("EditText")), index)).perform(click()); - return this; - } - - public FormEntryPage activateTextQuestion(String questionText) { - onView(allOf(withClassName(endsWith("EditText")), isDescendantOfA(isQuestionView(questionText)))).perform(scrollTo(), click()); + public FormEntryPage clickOnQuestionField(String questionText) { + Matcher classMatcher = withClassName(endsWith("EditText")); + Matcher questionViewMatcher = isQuestionView(questionText); + Interactions.clickOn(allOf(classMatcher, isDescendantOfA(questionViewMatcher))); return this; } From 3cb44082ca65c7ea9ae6b8a8b2a6efcb78f78a24 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 6 Jun 2024 17:12:04 +0200 Subject: [PATCH 560/750] Naming improvements --- .../main/java/org/odk/collect/androidshared/system/UriExt.kt | 2 +- .../org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt b/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt index 99cc1709c57..5d8701ee35e 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt @@ -8,7 +8,7 @@ import java.io.File import java.io.FileOutputStream import java.io.IOException -fun Uri.toFile(contentResolver: ContentResolver, dest: File) { +fun Uri.copyToFile(contentResolver: ContentResolver, dest: File) { try { contentResolver.openInputStream(this)?.use { inputStream -> FileOutputStream(dest).use { outputStream -> diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index 0b632059048..40b5e366204 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -5,8 +5,8 @@ import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import org.odk.collect.androidshared.system.copyToFile import org.odk.collect.androidshared.system.getFileName -import org.odk.collect.androidshared.system.toFile import org.odk.collect.async.Scheduler import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.keys.ProjectKeys @@ -61,7 +61,7 @@ class OfflineMapLayersViewModel( uri.getFileName(contentResolver)?.let { fileName -> if (fileName.endsWith(MbtilesFile.FILE_EXTENSION)) { val layerFile = File(tempLayersDir, fileName).also { file -> - uri.toFile(contentResolver, file) + uri.copyToFile(contentResolver, file) } layers.add(ReferenceLayer(layerFile.absolutePath, layerFile, MbtilesFile.readName(layerFile) ?: layerFile.name)) } From 5881d1543d40ecc1c086026e119597df0f230f5d Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 6 Jun 2024 17:13:11 +0200 Subject: [PATCH 561/750] Ignore exceptions in the copyToFile method --- .../java/org/odk/collect/androidshared/system/UriExt.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt b/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt index 5d8701ee35e..11092645aac 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt @@ -3,10 +3,8 @@ package org.odk.collect.androidshared.system import android.content.ContentResolver import android.net.Uri import android.provider.OpenableColumns -import timber.log.Timber import java.io.File import java.io.FileOutputStream -import java.io.IOException fun Uri.copyToFile(contentResolver: ContentResolver, dest: File) { try { @@ -15,8 +13,8 @@ fun Uri.copyToFile(contentResolver: ContentResolver, dest: File) { inputStream.copyTo(outputStream) } } - } catch (e: IOException) { - Timber.e(e) + } catch (e: Exception) { + // ignore } } From cf8566aabcb1b395fe03538b92d3cb8ec6f10c87 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 6 Jun 2024 17:29:45 +0200 Subject: [PATCH 562/750] Removed redundant iconTint --- maps/src/main/res/layout/offline_map_layers_picker.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/maps/src/main/res/layout/offline_map_layers_picker.xml b/maps/src/main/res/layout/offline_map_layers_picker.xml index 5e86a2eeebe..2530b21e876 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker.xml @@ -115,7 +115,6 @@ android:text="@string/add_layer" android:layout_marginBottom="@dimen/margin_extra_small" app:icon="@drawable/ic_add_white_24" - app:iconTint="?colorAccent" app:layout_constraintBottom_toTopOf="@id/bottom_divider" app:layout_constraintStart_toEndOf="@id/guideline_start" /> From beda06b94fcb85364622dd80213a56b376459a31 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 6 Jun 2024 17:33:04 +0200 Subject: [PATCH 563/750] Added more margin between the list of layers and the 'Add layer' button --- maps/src/main/res/layout/offline_map_layers_picker.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/maps/src/main/res/layout/offline_map_layers_picker.xml b/maps/src/main/res/layout/offline_map_layers_picker.xml index 2530b21e876..44c525d0d96 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker.xml @@ -90,6 +90,7 @@ android:layout_width="0dp" android:layout_height="0dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + android:layout_marginBottom="@dimen/margin_extra_small" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toTopOf="@id/add_layer" From 4bf56609a47ebce419d985ecccb0f4eec91e48cc Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 6 Jun 2024 17:35:05 +0200 Subject: [PATCH 564/750] Added a new line at the end of the file --- maps/src/main/res/layout/offline_map_layers_picker.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps/src/main/res/layout/offline_map_layers_picker.xml b/maps/src/main/res/layout/offline_map_layers_picker.xml index 44c525d0d96..2c407c36d13 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker.xml @@ -148,4 +148,4 @@ app:layout_constraintBottom_toBottomOf="@id/save" app:layout_constraintEnd_toStartOf="@id/save" app:layout_constraintTop_toTopOf="@id/save" /> - \ No newline at end of file + From f3b8bc4a51b128a5b946b0ca97d3c21559804918 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 6 Jun 2024 19:19:12 +0200 Subject: [PATCH 565/750] Do not expose layer dirs in repository --- .../DirectoryReferenceLayerRepository.kt | 12 ++--- .../maps/layers/OfflineMapLayersImporter.kt | 8 +-- .../maps/layers/OfflineMapLayersViewModel.kt | 5 +- .../maps/layers/ReferenceLayerRepository.kt | 4 +- .../DirectoryReferenceLayerRepositoryTest.kt | 42 +++++++++++++++ .../layers/OfflineMapLayersImporterTest.kt | 54 ++++++++----------- .../layers/TestReferenceLayerRepository.kt | 13 ++--- 7 files changed, 78 insertions(+), 60 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt index 627f635882d..384385ffb35 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt @@ -28,12 +28,12 @@ class DirectoryReferenceLayerRepository( } } - override fun getSharedLayersDirPath(): String { - return sharedLayersDirPath - } - - override fun getProjectLayersDirPath(): String { - return projectLayersDirPath + override fun addLayer(file: File, shared: Boolean) { + if (shared) { + file.copyTo(File(sharedLayersDirPath, file.name), true) + } else { + file.copyTo(File(projectLayersDirPath, file.name), true) + } } private fun getAllFilesWithDirectory() = listOf(sharedLayersDirPath, projectLayersDirPath).flatMap { dir -> diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt index ce2ce693339..0066f79bc7f 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt @@ -40,13 +40,7 @@ class OfflineMapLayersImporter( } binding.addLayerButton.setOnClickListener { - val layersDir = if (binding.allProjectsOption.isChecked) { - referenceLayerRepository.getSharedLayersDirPath() - } else { - referenceLayerRepository.getProjectLayersDirPath() - } - - viewModel.importNewLayers(layersDir) + viewModel.importNewLayers(binding.allProjectsOption.isChecked) dismiss() } return binding.root diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index 40b5e366204..e58bcce56af 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -74,13 +74,12 @@ class OfflineMapLayersViewModel( ) } - fun importNewLayers(layersDir: String) { + fun importNewLayers(shared: Boolean) { _isLoading.value = true scheduler.immediate( background = { - val destDir = File(layersDir) tempLayersDir.listFiles()?.forEach { - it.copyTo(File(destDir, it.name), true) + referenceLayerRepository.addLayer(it, shared) } tempLayersDir.delete() _isLoading.postValue(false) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt index fa981fd7cc2..f8dadbaa540 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt @@ -6,9 +6,7 @@ interface ReferenceLayerRepository { fun getAll(): List fun get(id: String): ReferenceLayer? - - fun getSharedLayersDirPath(): String - fun getProjectLayersDirPath(): String + fun addLayer(file: File, shared: Boolean) } data class ReferenceLayer(val id: String, val file: File, val name: String) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index 154c8b6a840..10aa5d8513e 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -164,6 +164,48 @@ class DirectoryReferenceLayerRepositoryTest { assertThat(repository.get(fileLayer.id)!!.name, equalTo(file.name)) } + @Test + fun addLayer_movesFileToTheSharedLayersDir_whenSharedIsTrue() { + val sharedLayersDir = TempFiles.createTempDir() + val projectLayersDir = TempFiles.createTempDir() + val file = TempFiles.createTempFile().also { + it.writeText("blah") + } + + val repository = DirectoryReferenceLayerRepository( + sharedLayersDir.absolutePath, + projectLayersDir.absolutePath + ) { StubMapConfigurator() } + + repository.addLayer(file, true) + + assertThat(sharedLayersDir.listFiles().size, equalTo(1)) + assertThat(sharedLayersDir.listFiles()[0].name, equalTo(file.name)) + assertThat(sharedLayersDir.listFiles()[0].readText(), equalTo("blah")) + assertThat(projectLayersDir.listFiles().size, equalTo(0)) + } + + @Test + fun addLayer_movesFileToTheProjectLayersDir_whenSharedIsFalse() { + val sharedLayersDir = TempFiles.createTempDir() + val projectLayersDir = TempFiles.createTempDir() + val file = TempFiles.createTempFile().also { + it.writeText("blah") + } + + val repository = DirectoryReferenceLayerRepository( + sharedLayersDir.absolutePath, + projectLayersDir.absolutePath + ) { StubMapConfigurator() } + + repository.addLayer(file, false) + + assertThat(sharedLayersDir.listFiles().size, equalTo(0)) + assertThat(projectLayersDir.listFiles().size, equalTo(1)) + assertThat(projectLayersDir.listFiles()[0].name, equalTo(file.name)) + assertThat(projectLayersDir.listFiles()[0].readText(), equalTo("blah")) + } + private class StubMapConfigurator : MapConfigurator { private val files = mutableMapOf>() diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt index a1ae2b54cfa..ef76e2238c3 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt @@ -19,6 +19,10 @@ import org.hamcrest.MatcherAssert.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule import org.odk.collect.settings.InMemSettingsProvider @@ -33,7 +37,7 @@ import java.io.File @RunWith(AndroidJUnit4::class) class OfflineMapLayersImporterTest { private val scheduler = FakeScheduler() - private val referenceLayerRepository = TestReferenceLayerRepository() + private val referenceLayerRepository = mock() private val settingsProvider = InMemSettingsProvider() @get:Rule @@ -192,12 +196,8 @@ class OfflineMapLayersImporterTest { @Test fun `clicking the 'add layer' button moves the files to the shared layers dir if it is selected`() { - val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION).also { - it.writeText("blah1") - } - val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION).also { - it.writeText("blah2") - } + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) launchFragment().onFragment { it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri())) @@ -208,26 +208,20 @@ class OfflineMapLayersImporterTest { onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) scheduler.flush() - assertThat(File(referenceLayerRepository.getSharedLayersDirPath()).listFiles().size, equalTo(2)) - assertThat(File(referenceLayerRepository.getProjectLayersDirPath()).listFiles().size, equalTo(0)) - - val copiedFile1 = File(referenceLayerRepository.getSharedLayersDirPath(), file1.name) - assertThat(copiedFile1.exists(), equalTo(true)) - assertThat(copiedFile1.readText(), equalTo("blah1")) + val fileCaptor = argumentCaptor() + val booleanCaptor = argumentCaptor() - val copiedFile2 = File(referenceLayerRepository.getSharedLayersDirPath(), file2.name) - assertThat(copiedFile2.exists(), equalTo(true)) - assertThat(copiedFile2.readText(), equalTo("blah2")) + verify(referenceLayerRepository, times(2)).addLayer(fileCaptor.capture(), booleanCaptor.capture()) + assertThat(fileCaptor.allValues.any { file -> file.name == file1.name }, equalTo(true)) + assertThat(fileCaptor.allValues.any { file -> file.name == file2.name }, equalTo(true)) + assertThat(booleanCaptor.firstValue, equalTo(true)) + assertThat(booleanCaptor.secondValue, equalTo(true)) } @Test fun `clicking the 'add layer' button moves the files to the project layers dir if it is selected`() { - val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION).also { - it.writeText("blah1") - } - val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION).also { - it.writeText("blah2") - } + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) launchFragment().onFragment { it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri())) @@ -240,16 +234,14 @@ class OfflineMapLayersImporterTest { onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) scheduler.flush() - assertThat(File(referenceLayerRepository.getSharedLayersDirPath()).listFiles().size, equalTo(0)) - assertThat(File(referenceLayerRepository.getProjectLayersDirPath()).listFiles().size, equalTo(2)) - - val copiedFile1 = File(referenceLayerRepository.getProjectLayersDirPath(), file1.name) - assertThat(copiedFile1.exists(), equalTo(true)) - assertThat(copiedFile1.readText(), equalTo("blah1")) + val fileCaptor = argumentCaptor() + val booleanCaptor = argumentCaptor() - val copiedFile2 = File(referenceLayerRepository.getProjectLayersDirPath(), file2.name) - assertThat(copiedFile2.exists(), equalTo(true)) - assertThat(copiedFile2.readText(), equalTo("blah2")) + verify(referenceLayerRepository, times(2)).addLayer(fileCaptor.capture(), booleanCaptor.capture()) + assertThat(fileCaptor.allValues.any { file -> file.name == file1.name }, equalTo(true)) + assertThat(fileCaptor.allValues.any { file -> file.name == file2.name }, equalTo(true)) + assertThat(booleanCaptor.firstValue, equalTo(false)) + assertThat(booleanCaptor.secondValue, equalTo(false)) } private fun launchFragment(): FragmentScenario { diff --git a/maps/src/test/java/org/odk/collect/maps/layers/TestReferenceLayerRepository.kt b/maps/src/test/java/org/odk/collect/maps/layers/TestReferenceLayerRepository.kt index 2226f0c854c..af87b0c2311 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/TestReferenceLayerRepository.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/TestReferenceLayerRepository.kt @@ -1,11 +1,8 @@ package org.odk.collect.maps.layers -import org.odk.collect.shared.TempFiles +import java.io.File class TestReferenceLayerRepository : ReferenceLayerRepository { - private val sharedLayersDirPath: String = TempFiles.createTempDir().absolutePath - private val projectLayersDirPath: String = TempFiles.createTempDir().absolutePath - private val layers = mutableListOf() override fun getAll(): List { @@ -16,12 +13,8 @@ class TestReferenceLayerRepository : ReferenceLayerRepository { return layers.find { it.id == id } } - override fun getSharedLayersDirPath(): String { - return sharedLayersDirPath - } - - override fun getProjectLayersDirPath(): String { - return projectLayersDirPath + override fun addLayer(file: File, shared: Boolean) { + TODO("Not yet implemented") } fun addLayers(vararg newLayers: ReferenceLayer) { From 6bfa2e1ca57277a1e052174331a24d0d7345f1c0 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 6 Jun 2024 19:44:59 +0200 Subject: [PATCH 566/750] Simplified DirectoryReferenceLayerRepositoryTest --- .../DirectoryReferenceLayerRepositoryTest.kt | 154 +++++++----------- 1 file changed, 62 insertions(+), 92 deletions(-) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index 10aa5d8513e..e8e52ce54f2 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -13,56 +13,56 @@ import org.odk.collect.shared.settings.Settings import java.io.File class DirectoryReferenceLayerRepositoryTest { + private val sharedLayersDir = TempFiles.createTempDir() + private val projectLayersDir = TempFiles.createTempDir() + private val mapConfigurator = StubMapConfigurator() + private val repository = DirectoryReferenceLayerRepository( + sharedLayersDir.absolutePath, + projectLayersDir.absolutePath + ) { mapConfigurator } + @Test fun getAll_returnsAllSupportedLayersInTheDirectory() { - val dir = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir) - val file2 = TempFiles.createTempFile(dir) - val file3 = TempFiles.createTempFile(dir) - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file1, true, file1.name) - it.addFile(file2, false, file2.name) - it.addFile(file3, true, file3.name) + val file1 = TempFiles.createTempFile(sharedLayersDir) + val file2 = TempFiles.createTempFile(sharedLayersDir) + val file3 = TempFiles.createTempFile(sharedLayersDir) + mapConfigurator.apply { + addFile(file1, true, file1.name) + addFile(file2, false, file2.name) + addFile(file3, true, file3.name) } - val repository = DirectoryReferenceLayerRepository(dir.absolutePath, "") { mapConfigurator } assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3)) } @Test fun getAll_returnsAllSupportedLayersInSubDirectories() { - val dir1 = TempFiles.createTempDir() - val dir2 = TempFiles.createTempDir(dir1) - val file1 = TempFiles.createTempFile(dir2) - val file2 = TempFiles.createTempFile(dir2) - val file3 = TempFiles.createTempFile(dir2) - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file1, true, file1.name) - it.addFile(file2, false, file2.name) - it.addFile(file3, true, file3.name) - } - - val repository = DirectoryReferenceLayerRepository(dir1.absolutePath, "") { mapConfigurator } + val subDir = TempFiles.createTempDir(sharedLayersDir) + val file1 = TempFiles.createTempFile(subDir) + val file2 = TempFiles.createTempFile(subDir) + val file3 = TempFiles.createTempFile(subDir) + mapConfigurator.apply { + addFile(file1, true, file1.name) + addFile(file2, false, file2.name) + addFile(file3, true, file3.name) + } + assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3)) } @Test fun getAll_withMultipleDirectories_returnsAllSupportedLayersInAllDirectories() { - val dir1 = TempFiles.createTempDir() - val dir2 = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir1) - val file2 = TempFiles.createTempFile(dir1) - val file3 = TempFiles.createTempFile(dir2) - val file4 = TempFiles.createTempFile(dir2) - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file1, true, file1.name) - it.addFile(file2, false, file2.name) - it.addFile(file3, true, file3.name) - it.addFile(file4, false, file4.name) - } - - val repository = - DirectoryReferenceLayerRepository(dir1.absolutePath, dir2.absolutePath) { mapConfigurator } + val file1 = TempFiles.createTempFile(sharedLayersDir) + val file2 = TempFiles.createTempFile(sharedLayersDir) + val file3 = TempFiles.createTempFile(projectLayersDir) + val file4 = TempFiles.createTempFile(projectLayersDir) + mapConfigurator.apply { + addFile(file1, true, file1.name) + addFile(file2, false, file2.name) + addFile(file3, true, file3.name) + addFile(file4, false, file4.name) + } + assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3)) } @@ -73,76 +73,62 @@ class DirectoryReferenceLayerRepositoryTest { */ @Test fun getAll_withMultipleDirectoriesWithFilesWithTheSameRelativePath_onlyReturnsTheSupportedFileFromTheFirstDirectory() { - val dir1 = TempFiles.createTempDir() - val dir2 = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir1, "blah", ".temp") - val file2 = TempFiles.createTempFile(dir2, "blah", ".temp") - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file1, true, file1.name) - it.addFile(file2, true, file2.name) + val file1 = TempFiles.createTempFile(sharedLayersDir, "blah", ".temp") + val file2 = TempFiles.createTempFile(projectLayersDir, "blah", ".temp") + mapConfigurator.apply { + addFile(file1, true, file1.name) + addFile(file2, true, file2.name) } - val repository = - DirectoryReferenceLayerRepository(dir1.absolutePath, dir2.absolutePath) { mapConfigurator } assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1)) } @Test fun get_returnsLayer() { - val dir = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir) - val file2 = TempFiles.createTempFile(dir) - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file1, true, file1.name) - it.addFile(file2, true, file2.name) + val file1 = TempFiles.createTempFile(sharedLayersDir) + val file2 = TempFiles.createTempFile(sharedLayersDir) + mapConfigurator.apply { + addFile(file1, true, file1.name) + addFile(file2, true, file2.name) } - val repository = DirectoryReferenceLayerRepository(dir.absolutePath, "") { mapConfigurator } val file2Layer = repository.getAll().first { it.file == file2 } assertThat(repository.get(file2Layer.id)!!.file, equalTo(file2)) } @Test fun get_withMultipleDirectories_returnsLayer() { - val dir1 = TempFiles.createTempDir() - val dir2 = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir1) - val file2 = TempFiles.createTempFile(dir2) - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file1, true, file1.name) - it.addFile(file2, true, file2.name) + val file1 = TempFiles.createTempFile(sharedLayersDir) + val file2 = TempFiles.createTempFile(projectLayersDir) + mapConfigurator.apply { + addFile(file1, true, file1.name) + addFile(file2, true, file2.name) } - val repository = DirectoryReferenceLayerRepository(dir1.absolutePath, dir2.absolutePath) { mapConfigurator } val file2Layer = repository.getAll().first { it.file == file2 } assertThat(repository.get(file2Layer.id)!!.file, equalTo(file2)) } @Test fun get_withMultipleDirectoriesWithFilesWithTheSameRelativePath_returnsLayerFromFirstDirectory() { - val dir1 = TempFiles.createTempDir() - val dir2 = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir1, "blah", ".temp") - val file2 = TempFiles.createTempFile(dir2, "blah", ".temp") - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file1, true, file1.name) - it.addFile(file2, true, file2.name) + val file1 = TempFiles.createTempFile(sharedLayersDir, "blah", ".temp") + val file2 = TempFiles.createTempFile(projectLayersDir, "blah", ".temp") + mapConfigurator.apply { + addFile(file1, true, file1.name) + addFile(file2, true, file2.name) } - val repository = DirectoryReferenceLayerRepository(dir1.absolutePath, dir2.absolutePath) { mapConfigurator } val layerId = repository.getAll().first().id assertThat(repository.get(layerId)!!.file, equalTo(file1)) } @Test fun get_whenFileDoesNotExist_returnsNull() { - val dir = TempFiles.createTempDir() - val file = TempFiles.createTempFile(dir) - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file, true, file.name) + val file = TempFiles.createTempFile(sharedLayersDir) + mapConfigurator.apply { + addFile(file, true, file.name) } - val repository = DirectoryReferenceLayerRepository(dir.absolutePath, "") { mapConfigurator } val fileLayer = repository.getAll().first { it.file == file } file.delete() @@ -151,14 +137,12 @@ class DirectoryReferenceLayerRepositoryTest { @Test fun get_returnsLayerWithCorrectName() { - val dir = TempFiles.createTempDir() - val file = TempFiles.createTempFile(dir) + val file = TempFiles.createTempFile(sharedLayersDir) - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file, true, file.name) + mapConfigurator.apply { + addFile(file, true, file.name) } - val repository = DirectoryReferenceLayerRepository(dir.absolutePath, "") { mapConfigurator } val fileLayer = repository.getAll().first { it.file == file } assertThat(repository.get(fileLayer.id)!!.name, equalTo(file.name)) @@ -166,17 +150,10 @@ class DirectoryReferenceLayerRepositoryTest { @Test fun addLayer_movesFileToTheSharedLayersDir_whenSharedIsTrue() { - val sharedLayersDir = TempFiles.createTempDir() - val projectLayersDir = TempFiles.createTempDir() val file = TempFiles.createTempFile().also { it.writeText("blah") } - val repository = DirectoryReferenceLayerRepository( - sharedLayersDir.absolutePath, - projectLayersDir.absolutePath - ) { StubMapConfigurator() } - repository.addLayer(file, true) assertThat(sharedLayersDir.listFiles().size, equalTo(1)) @@ -187,17 +164,10 @@ class DirectoryReferenceLayerRepositoryTest { @Test fun addLayer_movesFileToTheProjectLayersDir_whenSharedIsFalse() { - val sharedLayersDir = TempFiles.createTempDir() - val projectLayersDir = TempFiles.createTempDir() val file = TempFiles.createTempFile().also { it.writeText("blah") } - val repository = DirectoryReferenceLayerRepository( - sharedLayersDir.absolutePath, - projectLayersDir.absolutePath - ) { StubMapConfigurator() } - repository.addLayer(file, false) assertThat(sharedLayersDir.listFiles().size, equalTo(0)) From ef62803add29308a4da1a67e663880c5a2c680d0 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 6 Jun 2024 19:49:06 +0200 Subject: [PATCH 567/750] Removed TestReferenceLayersRepository used only in one class --- .../maps/layers/OfflineMapLayersPickerTest.kt | 51 +++++++++++-------- .../layers/TestReferenceLayerRepository.kt | 25 --------- 2 files changed, 29 insertions(+), 47 deletions(-) delete mode 100644 maps/src/test/java/org/odk/collect/maps/layers/TestReferenceLayerRepository.kt diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index 655941a0fad..cfff10481f9 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -28,6 +28,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.whenever import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule import org.odk.collect.maps.R @@ -43,7 +44,7 @@ import org.odk.collect.webpage.ExternalWebPageHelper @RunWith(AndroidJUnit4::class) class OfflineMapLayersPickerTest { - private val referenceLayerRepository = TestReferenceLayerRepository() + private val referenceLayerRepository = mock() private val scheduler = FakeScheduler() private val settingsProvider = InMemSettingsProvider() private val externalWebPageHelper = mock() @@ -83,8 +84,8 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'cancel' button does not save the layer`() { - referenceLayerRepository.addLayers( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) ) launchOfflineMapLayersPicker() @@ -126,8 +127,8 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'save' button saves null when 'None' option is checked`() { - referenceLayerRepository.addLayers( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) ) launchOfflineMapLayersPicker() @@ -140,8 +141,8 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'save' button saves the layer id if any is checked`() { - referenceLayerRepository.addLayers( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) ) launchOfflineMapLayersPicker() @@ -155,8 +156,8 @@ class OfflineMapLayersPickerTest { @Test fun `when no layer id is saved in settings the 'None' option should be checked`() { - referenceLayerRepository.addLayers( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) ) launchOfflineMapLayersPicker() @@ -169,9 +170,11 @@ class OfflineMapLayersPickerTest { @Test fun `when layer id is saved in settings the layer it belongs to should be checked`() { - referenceLayerRepository.addLayers( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), - ReferenceLayer("2", TempFiles.createTempFile(), "layer2") + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), + ReferenceLayer("2", TempFiles.createTempFile(), "layer2") + ) ) settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, "2") @@ -228,9 +231,11 @@ class OfflineMapLayersPickerTest { @Test fun `if there are multiple layers all of them are displayed along with the 'None'`() { - referenceLayerRepository.addLayers( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), - ReferenceLayer("2", TempFiles.createTempFile(), "layer2") + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), + ReferenceLayer("2", TempFiles.createTempFile(), "layer2") + ) ) launchOfflineMapLayersPicker() @@ -245,8 +250,8 @@ class OfflineMapLayersPickerTest { @Test fun `checking layers sets selection correctly`() { - referenceLayerRepository.addLayers( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) ) launchOfflineMapLayersPicker() @@ -264,8 +269,8 @@ class OfflineMapLayersPickerTest { @Test fun `recreating maintains selection`() { - referenceLayerRepository.addLayers( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) ) val scenario = launchOfflineMapLayersPicker() @@ -347,9 +352,11 @@ class OfflineMapLayersPickerTest { EspressoHelpers.clickOnText(string.add_layer) scheduler.flush() onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(click()) - referenceLayerRepository.addLayers( - ReferenceLayer("1", file1, file1.name), - ReferenceLayer("2", file2, file2.name) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", TempFiles.createTempFile(), file1.name), + ReferenceLayer("2", TempFiles.createTempFile(), file2.name) + ) ) scheduler.flush() diff --git a/maps/src/test/java/org/odk/collect/maps/layers/TestReferenceLayerRepository.kt b/maps/src/test/java/org/odk/collect/maps/layers/TestReferenceLayerRepository.kt deleted file mode 100644 index af87b0c2311..00000000000 --- a/maps/src/test/java/org/odk/collect/maps/layers/TestReferenceLayerRepository.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.odk.collect.maps.layers - -import java.io.File - -class TestReferenceLayerRepository : ReferenceLayerRepository { - private val layers = mutableListOf() - - override fun getAll(): List { - return layers - } - - override fun get(id: String): ReferenceLayer? { - return layers.find { it.id == id } - } - - override fun addLayer(file: File, shared: Boolean) { - TODO("Not yet implemented") - } - - fun addLayers(vararg newLayers: ReferenceLayer) { - newLayers.forEach { newLayer -> - layers.add(newLayer) - } - } -} From 63a7a4bfb3969fa2585476c5d6d2532d5b56f423 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 7 Jun 2024 00:40:51 +0200 Subject: [PATCH 568/750] Improved UriExt and added tests --- .../collect/androidshared/system/UriExt.kt | 81 ++++++++++++++++--- .../androidshared/system/UriExtTest.kt | 44 ++++++++++ .../maps/layers/OfflineMapLayersImporter.kt | 2 +- .../maps/layers/OfflineMapLayersPicker.kt | 4 +- .../maps/layers/OfflineMapLayersViewModel.kt | 14 ++-- .../layers/OfflineMapLayersImporterTest.kt | 14 ++-- 6 files changed, 130 insertions(+), 29 deletions(-) create mode 100644 androidshared/src/test/java/org/odk/collect/androidshared/system/UriExtTest.kt diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt b/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt index 11092645aac..9244e13c53f 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt @@ -1,14 +1,17 @@ package org.odk.collect.androidshared.system import android.content.ContentResolver +import android.content.Context import android.net.Uri import android.provider.OpenableColumns +import android.webkit.MimeTypeMap +import androidx.core.net.toFile import java.io.File import java.io.FileOutputStream -fun Uri.copyToFile(contentResolver: ContentResolver, dest: File) { +fun Uri.copyToFile(context: Context, dest: File) { try { - contentResolver.openInputStream(this)?.use { inputStream -> + context.contentResolver.openInputStream(this)?.use { inputStream -> FileOutputStream(dest).use { outputStream -> inputStream.copyTo(outputStream) } @@ -18,19 +21,73 @@ fun Uri.copyToFile(contentResolver: ContentResolver, dest: File) { } } -fun Uri.getFileName(contentResolver: ContentResolver): String? { +fun Uri.getFileExtension(context: Context): String? { + var extension = getFileName(context)?.substringAfterLast(".", "") + + if (extension.isNullOrEmpty()) { + val mimeType = context.contentResolver.getType(this) + + extension = if (scheme == ContentResolver.SCHEME_CONTENT) { + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) + } else { + MimeTypeMap.getFileExtensionFromUrl(toString()) + } + + if (extension.isNullOrEmpty()) { + extension = mimeType?.substringAfterLast("/", "") + } + } + + return if (extension.isNullOrEmpty()) { + null + } else { + ".$extension" + } +} + +fun Uri.getFileName(context: Context): String? { var fileName: String? = null - if (scheme == ContentResolver.SCHEME_CONTENT) { - val cursor = contentResolver.query(this, null, null, null, null) - cursor.use { - if (it != null && it.moveToFirst()) { - val fileNameColumnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) - fileName = it.getString(fileNameColumnIndex) + + try { + when (scheme) { + ContentResolver.SCHEME_FILE -> fileName = toFile().name + ContentResolver.SCHEME_CONTENT -> { + val cursor = context.contentResolver.query(this, null, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val fileNameColumnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (fileNameColumnIndex != -1) { + fileName = it.getString(fileNameColumnIndex) + } + } + } + } + ContentResolver.SCHEME_ANDROID_RESOURCE -> { + // for uris like [android.resource://com.example.app/1234567890] + val resourceId = lastPathSegment?.toIntOrNull() + if (resourceId != null) { + fileName = context.resources.getResourceName(resourceId) + } else { + // for uris like [android.resource://com.example.app/raw/sample] + val packageName = authority + if (pathSegments.size >= 2) { + val resourceType = pathSegments[0] + val resourceEntryName = pathSegments[1] + val resId = context.resources.getIdentifier(resourceEntryName, resourceType, packageName) + if (resId != 0) { + fileName = context.resources.getResourceName(resId) + } + } + } } } + + if (fileName == null) { + fileName = path?.substringAfterLast("/") + } + } catch (e: Exception) { + // ignore } - if (fileName == null) { - fileName = path?.substringAfterLast("/") - } + return fileName } diff --git a/androidshared/src/test/java/org/odk/collect/androidshared/system/UriExtTest.kt b/androidshared/src/test/java/org/odk/collect/androidshared/system/UriExtTest.kt new file mode 100644 index 00000000000..9dafdde3ef4 --- /dev/null +++ b/androidshared/src/test/java/org/odk/collect/androidshared/system/UriExtTest.kt @@ -0,0 +1,44 @@ +package org.odk.collect.androidshared.system + +import android.app.Application +import androidx.core.net.toUri +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.odk.collect.shared.TempFiles + +@RunWith(AndroidJUnit4::class) +class UriExtTest { + private val context = ApplicationProvider.getApplicationContext() + + @Test + fun `copyToFile copies the source file to the target file`() { + val sourceFile = TempFiles.createTempFile().also { + it.writeText("blah") + } + val sourceFileUri = sourceFile.toUri() + val targetFile = TempFiles.createTempFile() + + sourceFileUri.copyToFile(context, targetFile) + assertThat(targetFile.readText(), equalTo(sourceFile.readText())) + } + + @Test + fun `getFileExtension returns file extension`() { + val file = TempFiles.createTempFile(".jpg") + val fileUri = file.toUri() + + assertThat(fileUri.getFileExtension(context), equalTo(".jpg")) + } + + @Test + fun `getFileName returns file name`() { + val file = TempFiles.createTempFile() + val fileUri = file.toUri() + + assertThat(fileUri.getFileName(context), equalTo(file.name)) + } +} diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt index 0066f79bc7f..a3ca996b4ba 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt @@ -21,7 +21,7 @@ class OfflineMapLayersImporter( val viewModel: OfflineMapLayersViewModel by activityViewModels { object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider, requireContext().contentResolver) as T + return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider) as T } } } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 739e1c932df..360b692c5c7 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -30,7 +30,7 @@ class OfflineMapLayersPicker( private val viewModel: OfflineMapLayersViewModel by activityViewModels { object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider, requireContext().contentResolver) as T + return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider) as T } } } @@ -39,7 +39,7 @@ class OfflineMapLayersPicker( private val getLayers = registerForActivityResult(ActivityResultContracts.GetMultipleContents(), registry) { uris -> if (uris.isNotEmpty()) { - viewModel.loadLayersToImport(uris) + viewModel.loadLayersToImport(uris, requireContext()) DialogFragmentUtils.showIfNotShowing( OfflineMapLayersImporter::class.java, childFragmentManager diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index e58bcce56af..f23e7bc6871 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -1,11 +1,12 @@ package org.odk.collect.maps.layers -import android.content.ContentResolver +import android.content.Context import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.odk.collect.androidshared.system.copyToFile +import org.odk.collect.androidshared.system.getFileExtension import org.odk.collect.androidshared.system.getFileName import org.odk.collect.async.Scheduler import org.odk.collect.settings.SettingsProvider @@ -16,8 +17,7 @@ import java.io.File class OfflineMapLayersViewModel( private val referenceLayerRepository: ReferenceLayerRepository, private val scheduler: Scheduler, - private val settingsProvider: SettingsProvider, - private val contentResolver: ContentResolver + private val settingsProvider: SettingsProvider ) : ViewModel() { private val _isLoading = MutableLiveData() val isLoading: LiveData = _isLoading @@ -49,7 +49,7 @@ class OfflineMapLayersViewModel( ) } - fun loadLayersToImport(uris: List) { + fun loadLayersToImport(uris: List, context: Context) { _isLoading.value = true scheduler.immediate( background = { @@ -58,10 +58,10 @@ class OfflineMapLayersViewModel( } val layers = mutableListOf() uris.forEach { uri -> - uri.getFileName(contentResolver)?.let { fileName -> - if (fileName.endsWith(MbtilesFile.FILE_EXTENSION)) { + if (uri.getFileExtension(context) == MbtilesFile.FILE_EXTENSION) { + uri.getFileName(context)?.let { fileName -> val layerFile = File(tempLayersDir, fileName).also { file -> - uri.copyToFile(contentResolver, file) + uri.copyToFile(context, file) } layers.add(ReferenceLayer(layerFile.absolutePath, layerFile, MbtilesFile.readName(layerFile) ?: layerFile.name)) } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt index ef76e2238c3..f304774db04 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt @@ -63,7 +63,7 @@ class OfflineMapLayersImporterTest { launchFragment().onFragment { scheduler.flush() assertThat(it.isVisible, equalTo(true)) - it.viewModel.loadLayersToImport(emptyList()) + it.viewModel.loadLayersToImport(emptyList(), it.requireContext()) onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) scheduler.flush() RobolectricHelpers.runLooper() @@ -77,7 +77,7 @@ class OfflineMapLayersImporterTest { val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) launchFragment().onFragment { - it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri())) + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext()) } onView(withId(org.odk.collect.maps.R.id.progress_indicator)).check(matches(isDisplayed())) @@ -150,7 +150,7 @@ class OfflineMapLayersImporterTest { val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) launchFragment().onFragment { - it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri())) + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext()) } scheduler.flush() @@ -166,7 +166,7 @@ class OfflineMapLayersImporterTest { val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) val scenario = launchFragment().onFragment { - it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri())) + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext()) } scheduler.flush() @@ -184,7 +184,7 @@ class OfflineMapLayersImporterTest { val file2 = TempFiles.createTempFile("layer2", ".txt") launchFragment().onFragment { - it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri())) + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext()) } scheduler.flush() @@ -200,7 +200,7 @@ class OfflineMapLayersImporterTest { val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) launchFragment().onFragment { - it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri())) + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext()) } scheduler.flush() @@ -224,7 +224,7 @@ class OfflineMapLayersImporterTest { val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) launchFragment().onFragment { - it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri())) + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext()) } scheduler.flush() From d647df0b08aeed347a9f9c3811d78ff457f264e5 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 7 Jun 2024 14:48:59 +0200 Subject: [PATCH 569/750] Added new icons --- .../main/res/drawable/ic_baseline_collapse_24.xml | 13 +++++++++++++ .../src/main/res/drawable/ic_baseline_expand_24.xml | 13 +++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 icons/src/main/res/drawable/ic_baseline_collapse_24.xml create mode 100644 icons/src/main/res/drawable/ic_baseline_expand_24.xml diff --git a/icons/src/main/res/drawable/ic_baseline_collapse_24.xml b/icons/src/main/res/drawable/ic_baseline_collapse_24.xml new file mode 100644 index 00000000000..aa1bf3d0920 --- /dev/null +++ b/icons/src/main/res/drawable/ic_baseline_collapse_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/icons/src/main/res/drawable/ic_baseline_expand_24.xml b/icons/src/main/res/drawable/ic_baseline_expand_24.xml new file mode 100644 index 00000000000..52067c970f8 --- /dev/null +++ b/icons/src/main/res/drawable/ic_baseline_expand_24.xml @@ -0,0 +1,13 @@ + + + + From b24676ca7d383ae1fc6dae1a81a69d9102a94b73 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 7 Jun 2024 15:03:45 +0200 Subject: [PATCH 570/750] Updated the layout --- .../layout/offline_map_layers_picker_item.xml | 55 +++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/maps/src/main/res/layout/offline_map_layers_picker_item.xml b/maps/src/main/res/layout/offline_map_layers_picker_item.xml index 664415f42f5..879cdc91cdc 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker_item.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker_item.xml @@ -7,14 +7,59 @@ + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + - \ No newline at end of file + From 0e3ae3fff08c8f1c358f039b003a167cb96bf938 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 30 Apr 2024 16:41:49 +0100 Subject: [PATCH 571/750] Isolate deprecated network info code --- .../network/ConnectivityProvider.kt | 16 ++++++---- .../network/NetworkStateProvider.kt | 4 +-- .../autosend/AutoSendSettingsProvider.kt | 8 ++--- .../autosend/AutoSendSettingsProviderTest.kt | 29 +++++++------------ 4 files changed, 28 insertions(+), 29 deletions(-) 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 index 39faf56766e..020dd81848b 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/network/ConnectivityProvider.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/network/ConnectivityProvider.kt @@ -2,17 +2,23 @@ package org.odk.collect.androidshared.network import android.content.Context import android.net.ConnectivityManager -import android.net.NetworkInfo +import org.odk.collect.async.Scheduler class ConnectivityProvider(private val context: Context) : NetworkStateProvider { override val isDeviceOnline: Boolean get() { - val networkInfo = networkInfo - return networkInfo != null && networkInfo.isConnected + val activeNetworkInfo = connectivityManager.activeNetworkInfo + return activeNetworkInfo != null && activeNetworkInfo.isConnected } - override val networkInfo: NetworkInfo? - get() = connectivityManager.activeNetworkInfo + override val currentNetwork: Scheduler.NetworkType? + get() { + return when (connectivityManager.activeNetworkInfo?.type) { + ConnectivityManager.TYPE_WIFI -> Scheduler.NetworkType.WIFI + ConnectivityManager.TYPE_MOBILE -> Scheduler.NetworkType.CELLULAR + else -> null + } + } 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 index d3b9e983c58..8a2b4db3a8f 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/network/NetworkStateProvider.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/network/NetworkStateProvider.kt @@ -1,8 +1,8 @@ package org.odk.collect.androidshared.network -import android.net.NetworkInfo +import org.odk.collect.async.Scheduler interface NetworkStateProvider { val isDeviceOnline: Boolean - val networkInfo: NetworkInfo? + val currentNetwork: Scheduler.NetworkType? } 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..837579b282d 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.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/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..4a60a6e1b69 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 @@ -12,6 +10,7 @@ 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.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) From dde08ad10a2f2d9edccf94a384fb30e3a4ab109c Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 30 Apr 2024 17:25:44 +0100 Subject: [PATCH 572/750] Make TestScheduler network aware to reveal failing autosend tests --- .../formmanagement/BulkFinalizationTest.kt | 5 +++- .../instancemanagement/AutoSendTest.kt | 26 +++++++++++++++--- .../settings/ConfigureWithQRCodeTest.java | 25 +++++++---------- .../support/FakeNetworkStateProvider.kt | 25 +++++++++++++++++ .../android/support/TestDependencies.java | 3 ++- .../collect/android/support/TestScheduler.kt | 27 ++++++++++++------- .../android/support/pages/MainMenuPage.java | 4 +-- 7 files changed, 81 insertions(+), 34 deletions(-) create mode 100644 collect_app/src/androidTest/java/org/odk/collect/android/support/FakeNetworkStateProvider.kt 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..acdad2ac6df 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") 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..d1138394088 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,22 @@ class AutoSendTest { } @Test - fun whenFormHasAutoSend_fillingAndFinalizingForm_notifiesUserWhenSendingFails() { + 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) 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..2ac0de10d5d --- /dev/null +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeNetworkStateProvider.kt @@ -0,0 +1,25 @@ +package org.odk.collect.android.support + +import org.odk.collect.androidshared.network.NetworkStateProvider +import org.odk.collect.async.Scheduler + +class FakeNetworkStateProvider : NetworkStateProvider { + + private var online = true + private var type: Scheduler.NetworkType? = Scheduler.NetworkType.WIFI + + fun goOnline(networkType: Scheduler.NetworkType) { + online = true + type = networkType + } + + fun goOffline() { + online = false + type = null + } + + override val isDeviceOnline: Boolean + get() = online + 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..cccc4760708 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 @@ -16,7 +16,8 @@ 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(); 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..22d96a5df2c 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 @@ -7,6 +7,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Runnable import kotlinx.coroutines.flow.Flow +import org.odk.collect.androidshared.network.NetworkStateProvider import org.odk.collect.async.Cancellable import org.odk.collect.async.CoroutineAndWorkManagerScheduler import org.odk.collect.async.Scheduler @@ -15,7 +16,7 @@ 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,7 @@ class TestScheduler : Scheduler, CoroutineDispatcher() { inputData: Map, networkConstraint: Scheduler.NetworkType? ) { - deferredTasks.add(DeferredTask(tag, spec, null, inputData)) + deferredTasks.add(DeferredTask(tag, spec, null, inputData, networkConstraint)) } override fun networkDeferredRepeat( @@ -65,7 +66,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 +78,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 +136,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()); From f1bca3e83e5b3e4b50d811607f72b2d889205ba1 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 1 May 2024 12:15:30 +0100 Subject: [PATCH 573/750] Fix form level auto send --- .../instancemanagement/AutoSendTest.kt | 20 +++++++++ .../activities/FormEntryViewModelFactory.kt | 7 ++- .../activities/FormFillingActivity.java | 7 +-- .../backgroundwork/AutoSendFormTaskSpec.kt | 43 +++++++++++++++++++ .../FormUpdateAndInstanceSubmitScheduler.java | 12 ++++++ .../InstanceSubmitScheduler.java | 2 + .../android/backgroundwork/TaskData.kt | 1 + .../formentry/saving/FormSaveViewModel.java | 11 ++++- .../formhierarchy/FormHierarchyActivity.java | 7 ++- .../config/AppDependencyComponent.java | 3 ++ .../InstancesDataService.kt | 21 +++++++++ .../forms/one-question-autosend-disabled.xml | 20 +++++++++ 12 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 collect_app/src/main/java/org/odk/collect/android/backgroundwork/AutoSendFormTaskSpec.kt create mode 100644 test-forms/src/main/resources/forms/one-question-autosend-disabled.xml 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 d1138394088..6dffe359bde 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 @@ -153,6 +153,26 @@ class AutoSendTest { ) } + @Test + fun whenFormHasAutoSendDisabled_fillingAndFinalizingForm_doesNotSendForm_regardlessOfSetting() { + testDependencies.server.alwaysReturnError() + + 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/main/java/org/odk/collect/android/activities/FormEntryViewModelFactory.kt b/collect_app/src/main/java/org/odk/collect/android/activities/FormEntryViewModelFactory.kt index 679f8a51723..062767d8181 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 @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.savedstate.SavedStateRegistryOwner import org.javarosa.core.model.actions.recordaudio.RecordAudioActions import org.javarosa.core.model.instance.TreeReference +import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler import org.odk.collect.android.entities.EntitiesRepositoryProvider import org.odk.collect.android.formentry.BackgroundAudioViewModel import org.odk.collect.android.formentry.BackgroundAudioViewModel.RecordAudioActionRegistry @@ -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 instanceSubmitScheduler: InstanceSubmitScheduler ) : 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), + instanceSubmitScheduler ) } 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..ab3b45f5391 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 @@ -433,7 +433,8 @@ public void onCreate(Bundle savedInstanceState) { instancesRepositoryProvider, new SavepointsRepositoryProvider(this, storagePathProvider), new QRCodeCreatorImpl(), - new HtmlPrinter() + new HtmlPrinter(), + instanceSubmitScheduler ); 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/backgroundwork/AutoSendFormTaskSpec.kt b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/AutoSendFormTaskSpec.kt new file mode 100644 index 00000000000..8c3afc724ed --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/AutoSendFormTaskSpec.kt @@ -0,0 +1,43 @@ +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 AutoSendFormTaskSpec : TaskSpec { + + @Inject + lateinit var instancesDataService: InstancesDataService + + override val maxRetries: Int? = null + override val backoffPolicy = BackoffPolicy.EXPONENTIAL + override val backoffDelay: Long = 60_000 + + override fun getTask( + context: Context, + inputData: Map, + isLastUniqueExecution: Boolean + ): Supplier { + DaggerUtils.getComponent(context).inject(this) + + return Supplier { + val projectId = inputData[TaskData.DATA_PROJECT_ID]!! + val instanceId = inputData[TaskData.DATA_INSTANCE_ID]!!.toLong() + instancesDataService.sendInstances(projectId, listOf(instanceId)) + true + } + } + + override fun getWorkManagerAdapter(): Class { + return Adapter::class.java + } + + class Adapter(context: Context, workerParams: WorkerParameters) : + WorkerAdapter(AutoSendFormTaskSpec(), 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..0d46a79b4c2 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 @@ -83,6 +83,14 @@ public void scheduleSubmit(String projectId) { scheduler.networkDeferred(getAutoSendTag(projectId), new AutoSendTaskSpec(), inputData, networkType); } + @Override + public void scheduleSubmit(String projectId, Long instanceId) { + HashMap inputData = new HashMap<>(); + inputData.put(TaskData.DATA_PROJECT_ID, projectId); + inputData.put(TaskData.DATA_INSTANCE_ID, instanceId.toString()); + scheduler.networkDeferred(getAutoSendFormTag(projectId), new AutoSendFormTaskSpec(), inputData, null); + } + @Override public void cancelSubmit(String projectId) { scheduler.cancelDeferred(getAutoSendTag(projectId)); @@ -93,6 +101,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..3ee8a9d90fb 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 @@ -4,5 +4,7 @@ public interface InstanceSubmitScheduler { void scheduleSubmit(String projectId); + void scheduleSubmit(String projectId, Long instanceId); + void cancelSubmit(String projectId); } 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..84460388517 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_INSTANCE_ID = "instanceId" } 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..429f05a549c 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 @@ -17,6 +17,7 @@ import org.apache.commons.io.IOUtils; import org.javarosa.form.api.FormEntryController; import org.odk.collect.android.application.Collect; +import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler; import org.odk.collect.android.dao.helpers.InstancesDaoHelper; import org.odk.collect.android.dynamicpreload.ExternalDataManager; import org.odk.collect.android.formentry.FormSession; @@ -90,12 +91,13 @@ public class FormSaveViewModel extends ViewModel implements MaterialProgressDial private Form form; private Instance instance; private final Cancellable formSessionObserver; + private InstanceSubmitScheduler instanceSubmitScheduler; 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, InstanceSubmitScheduler instanceSubmitScheduler ) { 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.instanceSubmitScheduler = instanceSubmitScheduler; if (stateHandle.get(ORIGINAL_FILES) != null) { originalFiles = stateHandle.get(ORIGINAL_FILES); @@ -268,6 +271,12 @@ 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()); + + if (form.getAutoSend() != null && form.getAutoSend().equals("true")) { + instanceSubmitScheduler.scheduleSubmit(projectsDataService.getCurrentProject().getUuid(), instance.getDbId()); + } else { + instanceSubmitScheduler.scheduleSubmit(projectsDataService.getCurrentProject().getUuid()); + } } 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..7b2454c968c 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 @@ -47,6 +47,7 @@ import org.odk.collect.analytics.Analytics; import org.odk.collect.android.R; import org.odk.collect.android.activities.FormEntryViewModelFactory; +import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler; import org.odk.collect.android.entities.EntitiesRepositoryProvider; import org.odk.collect.android.exception.JavaRosaException; import org.odk.collect.android.formentry.FormEntryViewModel; @@ -195,6 +196,9 @@ public class FormHierarchyActivity extends LocalizedActivity implements DeleteRe @Inject public StoragePathProvider storagePathProvider; + @Inject + public InstanceSubmitScheduler instanceSubmitScheduler; + 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(), + instanceSubmitScheduler ); this.getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() 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..75d6f2d3c2c 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,6 +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.AutoSendFormTaskSpec; import org.odk.collect.android.backgroundwork.AutoSendTaskSpec; import org.odk.collect.android.backgroundwork.AutoUpdateTaskSpec; import org.odk.collect.android.backgroundwork.SyncFormsTaskSpec; @@ -261,6 +262,8 @@ interface Builder { void inject(DownloadFormListTask downloadFormListTask); + void inject(AutoSendFormTaskSpec autoSendFormTaskSpec); + OpenRosaHttpInterface openRosaHttpInterface(); ReferenceManager referenceManager(); 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..0da2fa62804 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 @@ -178,6 +178,27 @@ class InstancesDataService( } } + fun sendInstances(projectId: String, instanceIds: List) { + val projectDependencyProvider = + projectDependencyProviderFactory.create(projectId) + + val instanceSubmitter = InstanceSubmitter( + projectDependencyProvider.formsRepository, + projectDependencyProvider.generalSettings, + propertyManager, + httpInterface, + projectDependencyProvider.instancesRepository + ) + + val instances = instanceIds.map { + projectDependencyProvider.instancesRepository.get(it)!! + } + + val results = instanceSubmitter.submitInstances(instances) + notifier.onSubmission(results, projectDependencyProvider.projectId) + update(projectId) + } + fun autoSendInstances(projectId: String): Boolean { val projectDependencyProvider = projectDependencyProviderFactory.create(projectId) 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 + + + + + + + + + + + + + + + + From 8ae1ec2d5b1b641abb3fdbf2f7cd3ad3fa1c91a6 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 1 May 2024 12:48:27 +0100 Subject: [PATCH 574/750] Make sure form level auto send works for multiple forms --- .../instancemanagement/AutoSendTest.kt | 22 +++++++++++++++++++ .../collect/android/support/TestScheduler.kt | 1 + .../FormUpdateAndInstanceSubmitScheduler.java | 6 ++--- 3 files changed, 26 insertions(+), 3 deletions(-) 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 6dffe359bde..67427bb3707 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 @@ -120,6 +120,28 @@ class AutoSendTest { ).pressBack(MainMenuPage()) } + @Test + 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() 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 22d96a5df2c..1fb4f4f1773 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 @@ -56,6 +56,7 @@ class TestScheduler(private val networkStateProvider: NetworkStateProvider) : Sc inputData: Map, networkConstraint: Scheduler.NetworkType? ) { + cancelDeferred(tag) deferredTasks.add(DeferredTask(tag, spec, null, inputData, networkConstraint)) } 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 0d46a79b4c2..81e2d43004c 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 @@ -88,7 +88,7 @@ public void scheduleSubmit(String projectId, Long instanceId) { HashMap inputData = new HashMap<>(); inputData.put(TaskData.DATA_PROJECT_ID, projectId); inputData.put(TaskData.DATA_INSTANCE_ID, instanceId.toString()); - scheduler.networkDeferred(getAutoSendFormTag(projectId), new AutoSendFormTaskSpec(), inputData, null); + scheduler.networkDeferred(getAutoSendFormTag(projectId, instanceId), new AutoSendFormTaskSpec(), inputData, null); } @Override @@ -101,8 +101,8 @@ public String getAutoSendTag(String projectId) { return "AutoSendWorker:" + projectId; } - public String getAutoSendFormTag(String projectId) { - return "auto_send_form:" + projectId; + public String getAutoSendFormTag(String projectId, Long instanceId) { + return "auto_send_form:" + projectId + ":" + instanceId; } @NotNull From 77b9edb18dc86d54db9e01949e74ea8cb1d609a6 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 1 May 2024 13:03:31 +0100 Subject: [PATCH 575/750] Simplify auto send logic --- .../FormUpdateAndInstanceSubmitScheduler.java | 12 ++-- .../injection/config/AppDependencyModule.java | 2 +- .../InstancesDataService.kt | 10 +--- .../autosend/InstanceAutoSendFetcher.kt | 13 +---- ...ormUpdateAndInstanceSubmitSchedulerTest.kt | 11 ++++ .../audit/FormSaveViewModelTest.java | 8 +-- .../InstancesDataServiceTest.kt | 6 +- .../autosend/InstanceAutoSendFetcherTest.kt | 57 +------------------ 8 files changed, 33 insertions(+), 86 deletions(-) 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 81e2d43004c..005972b2cc3 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 @@ -69,18 +69,22 @@ public void cancelUpdates(String projectId) { @Override public void scheduleSubmit(String projectId) { - Scheduler.NetworkType networkType = null; + 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 AutoSendTaskSpec(), inputData, networkConstraint); } @Override 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..dcc7ead4828 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 @@ -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 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 0da2fa62804..d1529175930 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,13 @@ 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.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 +28,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) @@ -216,10 +214,8 @@ class InstancesDataService( ).withLock { acquiredLock: Boolean -> if (acquiredLock) { val toUpload = InstanceAutoSendFetcher.getInstancesToAutoSend( - application, projectDependencyProvider.instancesRepository, - projectDependencyProvider.formsRepository, - projectDependencyProvider.settingsProvider + projectDependencyProvider.formsRepository ) if (toUpload.isNotEmpty()) { 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..84b8c9619e1 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,32 +1,23 @@ package org.odk.collect.android.instancemanagement.autosend -import android.app.Application 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 + formsRepository: FormsRepository ): List { val allFinalizedForms = instancesRepository.getAllByStatus( Instance.STATUS_COMPLETE, Instance.STATUS_SUBMISSION_FAILED ) - val autoSendSetting = - settingsProvider.getUnprotectedSettings().getAutoSend(application) - return allFinalizedForms.filter { formsRepository.getLatestByFormIdAndVersion(it.formId, it.formVersion)?.let { form -> - form.shouldFormBeSentAutomatically(autoSendSetting != AutoSend.OFF) + form.autoSend == null } ?: false } } 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..4fd0f99afdd 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 @@ -127,6 +128,16 @@ class FormUpdateAndInstanceSubmitSchedulerTest { ) } + @Test + fun `scheduleSubmit does nothing if auto send is disabled`() { + settingsProvider.getUnprotectedSettings("myProject") + .save(ProjectKeys.KEY_AUTOSEND, "off") + val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) + + manager.scheduleSubmit("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/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/instancemanagement/InstancesDataServiceTest.kt b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/InstancesDataServiceTest.kt index 13358a88773..8b8f51be36e 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, 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..0001dcba000 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,16 @@ 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() - - val formWithEnabledAutoSendV2 = buildForm("1", "2", createTempDir().absolutePath, autosend = "true").build() - val instanceOfFormWithEnabledAutoSendCompleteV2 = buildInstance("1", "2", "instance 2", Instance.STATUS_COMPLETE, null, createTempDir().absolutePath).build() - - formsRepository.save(formWithEnabledAutoSendV1) - formsRepository.save(formWithEnabledAutoSendV2) - - instancesRepository.apply { - save(instanceOfFormWithEnabledAutoSendCompleteV1) - save(instanceOfFormWithEnabledAutoSendCompleteV2) - } - - val settingsProvider = InMemSettingsProvider().also { - it.getUnprotectedSettings() - .save(ProjectKeys.KEY_AUTOSEND, AutoSend.WIFI_ONLY.getValue(application)) - } - - val instancesToSend = InstanceAutoSendFetcher.getInstancesToAutoSend( - application, - instancesRepository, - formsRepository, - settingsProvider - ) - assertThat( - instancesToSend.map { it.instanceFilePath }, - contains( - instanceOfFormWithEnabledAutoSendCompleteV2.instanceFilePath ) ) } From 80a35a5b3303f075e63ffa0ad20f1bad6160bd4d Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 1 May 2024 15:14:25 +0100 Subject: [PATCH 576/750] Remove extra send method --- .../backgroundwork/AutoSendTaskSpec.kt | 2 +- .../FormUpdateAndInstanceSubmitScheduler.java | 2 +- ...endFormTaskSpec.kt => SendFormTaskSpec.kt} | 4 +- .../config/AppDependencyComponent.java | 4 +- .../InstancesDataService.kt | 37 ++++++------------- .../backgroundwork/AutoSendTaskSpecTest.kt | 4 +- .../InstancesDataServiceTest.kt | 12 +++--- 7 files changed, 25 insertions(+), 40 deletions(-) rename collect_app/src/main/java/org/odk/collect/android/backgroundwork/{AutoSendFormTaskSpec.kt => SendFormTaskSpec.kt} (92%) 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/AutoSendTaskSpec.kt index 6fbe7700c52..79266494f76 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/AutoSendTaskSpec.kt @@ -36,7 +36,7 @@ class AutoSendTaskSpec : TaskSpec { return Supplier { val projectId = inputData[TaskData.DATA_PROJECT_ID] if (projectId != null) { - instancesDataService.autoSendInstances(projectId) + instancesDataService.sendInstances(projectId) } else { throw IllegalArgumentException("No project ID provided!") } 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 005972b2cc3..41adb370c3c 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 @@ -92,7 +92,7 @@ public void scheduleSubmit(String projectId, Long instanceId) { HashMap inputData = new HashMap<>(); inputData.put(TaskData.DATA_PROJECT_ID, projectId); inputData.put(TaskData.DATA_INSTANCE_ID, instanceId.toString()); - scheduler.networkDeferred(getAutoSendFormTag(projectId, instanceId), new AutoSendFormTaskSpec(), inputData, null); + scheduler.networkDeferred(getAutoSendFormTag(projectId, instanceId), new SendFormTaskSpec(), inputData, null); } @Override diff --git a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/AutoSendFormTaskSpec.kt b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormTaskSpec.kt similarity index 92% rename from collect_app/src/main/java/org/odk/collect/android/backgroundwork/AutoSendFormTaskSpec.kt rename to collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormTaskSpec.kt index 8c3afc724ed..be1e51b1fd6 100644 --- a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/AutoSendFormTaskSpec.kt +++ b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormTaskSpec.kt @@ -10,7 +10,7 @@ import org.odk.collect.async.WorkerAdapter import java.util.function.Supplier import javax.inject.Inject -class AutoSendFormTaskSpec : TaskSpec { +class SendFormTaskSpec : TaskSpec { @Inject lateinit var instancesDataService: InstancesDataService @@ -39,5 +39,5 @@ class AutoSendFormTaskSpec : TaskSpec { } class Adapter(context: Context, workerParams: WorkerParameters) : - WorkerAdapter(AutoSendFormTaskSpec(), context, workerParams) + WorkerAdapter(SendFormTaskSpec(), context, workerParams) } 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 75d6f2d3c2c..2b60d9d844f 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.AutoSendFormTaskSpec; +import org.odk.collect.android.backgroundwork.SendFormTaskSpec; import org.odk.collect.android.backgroundwork.AutoSendTaskSpec; import org.odk.collect.android.backgroundwork.AutoUpdateTaskSpec; import org.odk.collect.android.backgroundwork.SyncFormsTaskSpec; @@ -262,7 +262,7 @@ interface Builder { void inject(DownloadFormListTask downloadFormListTask); - void inject(AutoSendFormTaskSpec autoSendFormTaskSpec); + void inject(SendFormTaskSpec sendFormTaskSpec); OpenRosaHttpInterface openRosaHttpInterface(); 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 d1529175930..ca390df9035 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 @@ -176,28 +176,7 @@ class InstancesDataService( } } - fun sendInstances(projectId: String, instanceIds: List) { - val projectDependencyProvider = - projectDependencyProviderFactory.create(projectId) - - val instanceSubmitter = InstanceSubmitter( - projectDependencyProvider.formsRepository, - projectDependencyProvider.generalSettings, - propertyManager, - httpInterface, - projectDependencyProvider.instancesRepository - ) - - val instances = instanceIds.map { - projectDependencyProvider.instancesRepository.get(it)!! - } - - val results = instanceSubmitter.submitInstances(instances) - notifier.onSubmission(results, projectDependencyProvider.projectId) - update(projectId) - } - - fun autoSendInstances(projectId: String): Boolean { + fun sendInstances(projectId: String, instanceIds: List? = null): Boolean { val projectDependencyProvider = projectDependencyProviderFactory.create(projectId) @@ -213,10 +192,16 @@ class InstancesDataService( projectDependencyProvider.projectId ).withLock { acquiredLock: Boolean -> if (acquiredLock) { - val toUpload = InstanceAutoSendFetcher.getInstancesToAutoSend( - projectDependencyProvider.instancesRepository, - projectDependencyProvider.formsRepository - ) + val toUpload = if (instanceIds != null) { + instanceIds.map { + projectDependencyProvider.instancesRepository.get(it)!! + } + } else { + InstanceAutoSendFetcher.getInstancesToAutoSend( + projectDependencyProvider.instancesRepository, + projectDependencyProvider.formsRepository + ) + } if (toUpload.isNotEmpty()) { val results = instanceSubmitter.submitInstances(toUpload) 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/AutoSendTaskSpecTest.kt index 6cf2c3091bb..65880ce73f0 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/AutoSendTaskSpecTest.kt @@ -54,7 +54,7 @@ class AutoSendTaskSpecTest { @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() @@ -64,7 +64,7 @@ class AutoSendTaskSpecTest { @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() 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 8b8f51be36e..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 @@ -92,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()) @@ -114,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)) } } From 4cd6530b9f047ec9ef4937fe12dfb85b3fa1e36d Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 1 May 2024 16:36:14 +0100 Subject: [PATCH 577/750] Use one task spec for sending forms --- .../FormUpdateAndInstanceSubmitScheduler.java | 4 +- .../backgroundwork/SendFormTaskSpec.kt | 43 ------------------- ...toSendTaskSpec.kt => SendFormsTaskSpec.kt} | 11 +++-- .../config/AppDependencyComponent.java | 7 +-- ...ormUpdateAndInstanceSubmitSchedulerTest.kt | 6 +-- ...skSpecTest.kt => SendFormsTaskSpecTest.kt} | 8 ++-- 6 files changed, 19 insertions(+), 60 deletions(-) delete mode 100644 collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormTaskSpec.kt rename collect_app/src/main/java/org/odk/collect/android/backgroundwork/{AutoSendTaskSpec.kt => SendFormsTaskSpec.kt} (81%) rename collect_app/src/test/java/org/odk/collect/android/backgroundwork/{AutoSendTaskSpecTest.kt => SendFormsTaskSpecTest.kt} (93%) 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 41adb370c3c..8ea2ef47555 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 @@ -84,7 +84,7 @@ public void scheduleSubmit(String projectId) { HashMap inputData = new HashMap<>(); inputData.put(TaskData.DATA_PROJECT_ID, projectId); - scheduler.networkDeferred(getAutoSendTag(projectId), new AutoSendTaskSpec(), inputData, networkConstraint); + scheduler.networkDeferred(getAutoSendTag(projectId), new SendFormsTaskSpec(), inputData, networkConstraint); } @Override @@ -92,7 +92,7 @@ public void scheduleSubmit(String projectId, Long instanceId) { HashMap inputData = new HashMap<>(); inputData.put(TaskData.DATA_PROJECT_ID, projectId); inputData.put(TaskData.DATA_INSTANCE_ID, instanceId.toString()); - scheduler.networkDeferred(getAutoSendFormTag(projectId, instanceId), new SendFormTaskSpec(), inputData, null); + scheduler.networkDeferred(getAutoSendFormTag(projectId, instanceId), new SendFormsTaskSpec(), inputData, null); } @Override diff --git a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormTaskSpec.kt b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormTaskSpec.kt deleted file mode 100644 index be1e51b1fd6..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormTaskSpec.kt +++ /dev/null @@ -1,43 +0,0 @@ -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 SendFormTaskSpec : TaskSpec { - - @Inject - lateinit var instancesDataService: InstancesDataService - - override val maxRetries: Int? = null - override val backoffPolicy = BackoffPolicy.EXPONENTIAL - override val backoffDelay: Long = 60_000 - - override fun getTask( - context: Context, - inputData: Map, - isLastUniqueExecution: Boolean - ): Supplier { - DaggerUtils.getComponent(context).inject(this) - - return Supplier { - val projectId = inputData[TaskData.DATA_PROJECT_ID]!! - val instanceId = inputData[TaskData.DATA_INSTANCE_ID]!!.toLong() - instancesDataService.sendInstances(projectId, listOf(instanceId)) - true - } - } - - override fun getWorkManagerAdapter(): Class { - return Adapter::class.java - } - - class Adapter(context: Context, workerParams: WorkerParameters) : - WorkerAdapter(SendFormTaskSpec(), context, workerParams) -} 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 81% 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 79266494f76..cf13497cfe4 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 @@ -23,7 +23,7 @@ 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 @@ -35,8 +35,13 @@ class AutoSendTaskSpec : TaskSpec { DaggerUtils.getComponent(context).inject(this) return Supplier { val projectId = inputData[TaskData.DATA_PROJECT_ID] + val instanceId = inputData[TaskData.DATA_INSTANCE_ID]?.toLong() if (projectId != null) { - instancesDataService.sendInstances(projectId) + if (instanceId != null) { + instancesDataService.sendInstances(projectId, listOf(instanceId)) + } else { + instancesDataService.sendInstances(projectId) + } } else { throw IllegalArgumentException("No project ID provided!") } @@ -48,5 +53,5 @@ class AutoSendTaskSpec : TaskSpec { } class Adapter(context: Context, workerParams: WorkerParameters) : - WorkerAdapter(AutoSendTaskSpec(), context, workerParams) + WorkerAdapter(SendFormsTaskSpec(), context, workerParams) } 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 2b60d9d844f..5ca03856628 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,8 +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.SendFormTaskSpec; -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; @@ -170,7 +169,7 @@ interface Builder { void inject(ShowQRCodeFragment showQRCodeFragment); - void inject(AutoSendTaskSpec autoSendTaskSpec); + void inject(SendFormsTaskSpec sendFormsTaskSpec); void inject(AdminPasswordDialogFragment adminPasswordDialogFragment); @@ -262,8 +261,6 @@ interface Builder { void inject(DownloadFormListTask downloadFormListTask); - void inject(SendFormTaskSpec sendFormTaskSpec); - OpenRosaHttpInterface openRosaHttpInterface(); ReferenceManager referenceManager(); 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 4fd0f99afdd..43e64d627b8 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 @@ -92,7 +92,7 @@ class FormUpdateAndInstanceSubmitSchedulerTest { manager.scheduleSubmit("myProject") verify(scheduler).networkDeferred( eq("AutoSendWorker:myProject"), - any(), + any(), eq(mapOf(TaskData.DATA_PROJECT_ID to "myProject")), eq(null) ) @@ -107,7 +107,7 @@ class FormUpdateAndInstanceSubmitSchedulerTest { manager.scheduleSubmit("myProject") verify(scheduler).networkDeferred( eq("AutoSendWorker:myProject"), - any(), + any(), eq(mapOf(TaskData.DATA_PROJECT_ID to "myProject")), eq(Scheduler.NetworkType.WIFI) ) @@ -122,7 +122,7 @@ class FormUpdateAndInstanceSubmitSchedulerTest { manager.scheduleSubmit("myProject") verify(scheduler).networkDeferred( eq("AutoSendWorker:myProject"), - any(), + any(), eq(mapOf(TaskData.DATA_PROJECT_ID to "myProject")), eq(Scheduler.NetworkType.CELLULAR) ) 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 93% 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 65880ce73f0..99f3d0da418 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 @@ -49,7 +49,7 @@ class AutoSendTaskSpecTest { @Test fun `maxRetries should not be limited`() { - assertThat(AutoSendTaskSpec().maxRetries, equalTo(null)) + assertThat(SendFormsTaskSpec().maxRetries, equalTo(null)) } @Test @@ -57,7 +57,7 @@ class AutoSendTaskSpecTest { 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)) } @@ -67,7 +67,7 @@ class AutoSendTaskSpecTest { 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)) } From 4889cd7113fba6da838568edfb586c4910fef961 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 1 May 2024 17:11:24 +0100 Subject: [PATCH 578/750] Support auto send forms in bulk finalization --- .../formmanagement/BulkFinalizationTest.kt | 20 +++++++++++++++++ .../activities/FormEntryViewModelFactory.kt | 6 ++--- .../activities/FormFillingActivity.java | 6 ++--- .../formentry/saving/FormSaveViewModel.java | 14 +++++------- .../formhierarchy/FormHierarchyActivity.java | 6 ++--- .../InstancesDataService.kt | 22 ++++++++++++++++++- 6 files changed, 55 insertions(+), 19 deletions(-) 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 acdad2ac6df..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 @@ -193,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/main/java/org/odk/collect/android/activities/FormEntryViewModelFactory.kt b/collect_app/src/main/java/org/odk/collect/android/activities/FormEntryViewModelFactory.kt index 062767d8181..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 @@ -6,7 +6,6 @@ import androidx.lifecycle.ViewModel import androidx.savedstate.SavedStateRegistryOwner import org.javarosa.core.model.actions.recordaudio.RecordAudioActions import org.javarosa.core.model.instance.TreeReference -import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler import org.odk.collect.android.entities.EntitiesRepositoryProvider import org.odk.collect.android.formentry.BackgroundAudioViewModel import org.odk.collect.android.formentry.BackgroundAudioViewModel.RecordAudioActionRegistry @@ -20,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 @@ -57,7 +57,7 @@ class FormEntryViewModelFactory( private val savepointsRepositoryProvider: SavepointsRepositoryProvider, private val qrCodeCreator: QRCodeCreator, private val htmlPrinter: HtmlPrinter, - private val instanceSubmitScheduler: InstanceSubmitScheduler + private val instancesDataService: InstancesDataService ) : AbstractSavedStateViewModelFactory(owner, null) { override fun create( @@ -89,7 +89,7 @@ class FormEntryViewModelFactory( entitiesRepositoryProvider.get(projectId), instancesRepositoryProvider.get(projectId), savepointsRepositoryProvider.get(projectId), - instanceSubmitScheduler + 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 ab3b45f5391..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; @@ -434,7 +434,7 @@ public void onCreate(Bundle savedInstanceState) { new SavepointsRepositoryProvider(this, storagePathProvider), new QRCodeCreatorImpl(), new HtmlPrinter(), - instanceSubmitScheduler + instancesDataService ); this.getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() 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 429f05a549c..c15e241c115 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 @@ -17,12 +17,12 @@ import org.apache.commons.io.IOUtils; import org.javarosa.form.api.FormEntryController; import org.odk.collect.android.application.Collect; -import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler; import org.odk.collect.android.dao.helpers.InstancesDaoHelper; import org.odk.collect.android.dynamicpreload.ExternalDataManager; 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; @@ -91,13 +91,13 @@ public class FormSaveViewModel extends ViewModel implements MaterialProgressDial private Form form; private Instance instance; private final Cancellable formSessionObserver; - private InstanceSubmitScheduler instanceSubmitScheduler; + 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, InstanceSubmitScheduler instanceSubmitScheduler + SavepointsRepository savepointsRepository, InstancesDataService instancesDataService ) { this.stateHandle = stateHandle; this.clock = clock; @@ -109,7 +109,7 @@ public FormSaveViewModel(SavedStateHandle stateHandle, Supplier clock, For this.entitiesRepository = entitiesRepository; this.instancesRepository = instancesRepository; this.savepointsRepository = savepointsRepository; - this.instanceSubmitScheduler = instanceSubmitScheduler; + this.instancesDataService = instancesDataService; if (stateHandle.get(ORIGINAL_FILES) != null) { originalFiles = stateHandle.get(ORIGINAL_FILES); @@ -272,11 +272,7 @@ private void handleTaskResult(SaveToDiskResult taskResult, SaveRequest saveReque formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.FORM_EXIT, false, clock.get()); formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.FORM_FINALIZE, true, clock.get()); - if (form.getAutoSend() != null && form.getAutoSend().equals("true")) { - instanceSubmitScheduler.scheduleSubmit(projectsDataService.getCurrentProject().getUuid(), instance.getDbId()); - } else { - instanceSubmitScheduler.scheduleSubmit(projectsDataService.getCurrentProject().getUuid()); - } + instancesDataService.instanceFinalized(projectsDataService.getCurrentProject().getUuid(), instance.getDbId()); } 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 7b2454c968c..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 @@ -47,7 +47,6 @@ import org.odk.collect.analytics.Analytics; import org.odk.collect.android.R; import org.odk.collect.android.activities.FormEntryViewModelFactory; -import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler; import org.odk.collect.android.entities.EntitiesRepositoryProvider; import org.odk.collect.android.exception.JavaRosaException; import org.odk.collect.android.formentry.FormEntryViewModel; @@ -55,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; @@ -197,7 +197,7 @@ public class FormHierarchyActivity extends LocalizedActivity implements DeleteRe public StoragePathProvider storagePathProvider; @Inject - public InstanceSubmitScheduler instanceSubmitScheduler; + public InstancesDataService instancesDataService; protected final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) { @Override @@ -235,7 +235,7 @@ public void onCreate(Bundle savedInstanceState) { new SavepointsRepositoryProvider(this, storagePathProvider), new QRCodeCreatorImpl(), new HtmlPrinter(), - instanceSubmitScheduler + instancesDataService ); this.getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() 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 ca390df9035..33d8c4344d2 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 @@ -120,6 +120,7 @@ class InstancesDataService( if (finalizedInstance == null) { result.copy(failureCount = result.failureCount + 1) } else { + instanceFinalized(projectId, finalizedInstance.dbId) result } } @@ -131,7 +132,6 @@ class InstancesDataService( } update(projectId) - instanceSubmitScheduler.scheduleSubmit(projectId) return result.copy(successCount = instances.size - result.failureCount) } @@ -218,6 +218,26 @@ class InstancesDataService( } } + fun instanceFinalized(projectId: String, instanceId: Long? = null) { + if (instanceId != null) { + val projectDependencyProvider = projectDependencyProviderFactory.create(projectId) + val formsRepository = projectDependencyProvider.formsRepository + val instancesRepository = projectDependencyProvider.instancesRepository + + val instance = instancesRepository.get(instanceId)!! + val form = + formsRepository.getLatestByFormIdAndVersion(instance.formId, instance.formVersion)!! + + if (form.autoSend != null && form.autoSend == "true") { + instanceSubmitScheduler.scheduleSubmit(projectId, instance.dbId) + } else { + instanceSubmitScheduler.scheduleSubmit(projectId) + } + } else { + instanceSubmitScheduler.scheduleSubmit(projectId) + } + } + companion object { private const val EDITABLE_COUNT_KEY = "instancesEditableCount" private const val SENDABLE_COUNT_KEY = "instancesSendableCount" From 7e902212f20d6b1189d482470e2a6f0628cfa477 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 1 May 2024 17:15:03 +0100 Subject: [PATCH 579/750] Rename method --- .../FormUpdateAndInstanceSubmitScheduler.java | 2 +- .../android/backgroundwork/InstanceSubmitScheduler.java | 2 +- .../android/instancemanagement/InstancesDataService.kt | 4 ++-- .../screens/FormManagementPreferencesFragment.java | 2 +- .../FormUpdateAndInstanceSubmitSchedulerTest.kt | 8 ++++---- .../screens/FormManagementPreferencesFragmentTest.kt | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) 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 8ea2ef47555..cc54f8a69e0 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,7 +68,7 @@ public void cancelUpdates(String projectId) { } @Override - public void scheduleSubmit(String projectId) { + public void scheduleSubmitIfNeeded(String projectId) { Scheduler.NetworkType networkConstraint; Settings settings = settingsProvider.getUnprotectedSettings(projectId); AutoSend autoSendSetting = StringIdEnumUtils.getAutoSend(settings, application); 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 3ee8a9d90fb..50fd93da93b 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,7 @@ public interface InstanceSubmitScheduler { - void scheduleSubmit(String projectId); + void scheduleSubmitIfNeeded(String projectId); void scheduleSubmit(String projectId, Long instanceId); 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 33d8c4344d2..7975a01b394 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 @@ -231,10 +231,10 @@ class InstancesDataService( if (form.autoSend != null && form.autoSend == "true") { instanceSubmitScheduler.scheduleSubmit(projectId, instance.dbId) } else { - instanceSubmitScheduler.scheduleSubmit(projectId) + instanceSubmitScheduler.scheduleSubmitIfNeeded(projectId) } } else { - instanceSubmitScheduler.scheduleSubmit(projectId) + instanceSubmitScheduler.scheduleSubmitIfNeeded(projectId) } } 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..87f95b34223 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.scheduleSubmitIfNeeded(projectsDataService.getCurrentProject().getUuid()); } } 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 43e64d627b8..f80505eae3b 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 @@ -89,7 +89,7 @@ class FormUpdateAndInstanceSubmitSchedulerTest { .save(ProjectKeys.KEY_AUTOSEND, "wifi_and_cellular") val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) - manager.scheduleSubmit("myProject") + manager.scheduleSubmitIfNeeded("myProject") verify(scheduler).networkDeferred( eq("AutoSendWorker:myProject"), any(), @@ -104,7 +104,7 @@ class FormUpdateAndInstanceSubmitSchedulerTest { .save(ProjectKeys.KEY_AUTOSEND, "wifi_only") val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) - manager.scheduleSubmit("myProject") + manager.scheduleSubmitIfNeeded("myProject") verify(scheduler).networkDeferred( eq("AutoSendWorker:myProject"), any(), @@ -119,7 +119,7 @@ class FormUpdateAndInstanceSubmitSchedulerTest { .save(ProjectKeys.KEY_AUTOSEND, "cellular_only") val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) - manager.scheduleSubmit("myProject") + manager.scheduleSubmitIfNeeded("myProject") verify(scheduler).networkDeferred( eq("AutoSendWorker:myProject"), any(), @@ -134,7 +134,7 @@ class FormUpdateAndInstanceSubmitSchedulerTest { .save(ProjectKeys.KEY_AUTOSEND, "off") val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) - manager.scheduleSubmit("myProject") + manager.scheduleSubmitIfNeeded("myProject") verifyNoInteractions(scheduler) } 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..039809ae089 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).scheduleSubmitIfNeeded(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()).scheduleSubmitIfNeeded(projectID) } } From c862c75271dcd853856de7a9cda9a8daabd15b9f Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 1 May 2024 19:23:20 +0100 Subject: [PATCH 580/750] Simplify method signature --- .../formentry/saving/FormSaveViewModel.java | 2 +- .../InstancesDataService.kt | 21 +++++-------------- 2 files changed, 6 insertions(+), 17 deletions(-) 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 c15e241c115..1e9e76a629d 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 @@ -272,7 +272,7 @@ private void handleTaskResult(SaveToDiskResult taskResult, SaveRequest saveReque 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(), instance.getDbId()); + instancesDataService.instanceFinalized(projectsDataService.getCurrentProject().getUuid(), form, instance); } else { formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.FORM_EXIT, true, clock.get()); } 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 7975a01b394..a25b2b25e7f 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 @@ -15,6 +15,7 @@ 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.AppState +import org.odk.collect.forms.Form import org.odk.collect.forms.instances.Instance import org.odk.collect.metadata.PropertyManager import java.io.File @@ -120,7 +121,7 @@ class InstancesDataService( if (finalizedInstance == null) { result.copy(failureCount = result.failureCount + 1) } else { - instanceFinalized(projectId, finalizedInstance.dbId) + instanceFinalized(projectId, form, instance) result } } @@ -218,21 +219,9 @@ class InstancesDataService( } } - fun instanceFinalized(projectId: String, instanceId: Long? = null) { - if (instanceId != null) { - val projectDependencyProvider = projectDependencyProviderFactory.create(projectId) - val formsRepository = projectDependencyProvider.formsRepository - val instancesRepository = projectDependencyProvider.instancesRepository - - val instance = instancesRepository.get(instanceId)!! - val form = - formsRepository.getLatestByFormIdAndVersion(instance.formId, instance.formVersion)!! - - if (form.autoSend != null && form.autoSend == "true") { - instanceSubmitScheduler.scheduleSubmit(projectId, instance.dbId) - } else { - instanceSubmitScheduler.scheduleSubmitIfNeeded(projectId) - } + fun instanceFinalized(projectId: String, form: Form, instance: Instance) { + if (form.autoSend != null && form.autoSend == "true") { + instanceSubmitScheduler.scheduleSubmit(projectId, instance.dbId) } else { instanceSubmitScheduler.scheduleSubmitIfNeeded(projectId) } From e2a81c6366c2ccca2f98ff7eba211010435420fc Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 2 May 2024 10:34:25 +0100 Subject: [PATCH 581/750] Don't queue multiple form level auto send jobs --- .../FormUpdateAndInstanceSubmitScheduler.java | 12 ++++++------ .../InstanceSubmitScheduler.java | 4 ++-- .../backgroundwork/SendFormsTaskSpec.kt | 6 +++--- .../collect/android/backgroundwork/TaskData.kt | 2 +- .../formentry/saving/FormSaveViewModel.java | 2 +- .../instancemanagement/InstancesDataService.kt | 18 ++++++++++-------- .../FormManagementPreferencesFragment.java | 2 +- ...FormUpdateAndInstanceSubmitSchedulerTest.kt | 8 ++++---- .../FormManagementPreferencesFragmentTest.kt | 4 ++-- 9 files changed, 30 insertions(+), 28 deletions(-) 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 cc54f8a69e0..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,7 +68,7 @@ public void cancelUpdates(String projectId) { } @Override - public void scheduleSubmitIfNeeded(String projectId) { + public void scheduleAutoSend(String projectId) { Scheduler.NetworkType networkConstraint; Settings settings = settingsProvider.getUnprotectedSettings(projectId); AutoSend autoSendSetting = StringIdEnumUtils.getAutoSend(settings, application); @@ -88,11 +88,11 @@ public void scheduleSubmitIfNeeded(String projectId) { } @Override - public void scheduleSubmit(String projectId, Long instanceId) { + public void scheduleFormAutoSend(String projectId) { HashMap inputData = new HashMap<>(); inputData.put(TaskData.DATA_PROJECT_ID, projectId); - inputData.put(TaskData.DATA_INSTANCE_ID, instanceId.toString()); - scheduler.networkDeferred(getAutoSendFormTag(projectId, instanceId), new SendFormsTaskSpec(), inputData, null); + inputData.put(TaskData.DATA_FORM_AUTO_SEND, ""); + scheduler.networkDeferred(getAutoSendFormTag(projectId), new SendFormsTaskSpec(), inputData, null); } @Override @@ -105,8 +105,8 @@ public String getAutoSendTag(String projectId) { return "AutoSendWorker:" + projectId; } - public String getAutoSendFormTag(String projectId, Long instanceId) { - return "auto_send_form:" + projectId + ":" + instanceId; + public String getAutoSendFormTag(String projectId) { + return "auto_send_form:" + projectId; } @NotNull 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 50fd93da93b..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,9 +2,9 @@ public interface InstanceSubmitScheduler { - void scheduleSubmitIfNeeded(String projectId); + void scheduleAutoSend(String projectId); - void scheduleSubmit(String projectId, Long instanceId); + void scheduleFormAutoSend(String projectId); void cancelSubmit(String projectId); } diff --git a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt index cf13497cfe4..1a40373750e 100644 --- a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt +++ b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt @@ -35,10 +35,10 @@ class SendFormsTaskSpec : TaskSpec { DaggerUtils.getComponent(context).inject(this) return Supplier { val projectId = inputData[TaskData.DATA_PROJECT_ID] - val instanceId = inputData[TaskData.DATA_INSTANCE_ID]?.toLong() + val formAutoSend = inputData[TaskData.DATA_FORM_AUTO_SEND] != null if (projectId != null) { - if (instanceId != null) { - instancesDataService.sendInstances(projectId, listOf(instanceId)) + if (formAutoSend) { + instancesDataService.sendInstances(projectId, formAutoSendOnly = true) } else { instancesDataService.sendInstances(projectId) } 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 84460388517..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,5 +2,5 @@ package org.odk.collect.android.backgroundwork object TaskData { const val DATA_PROJECT_ID = "projectId" - const val DATA_INSTANCE_ID = "instanceId" + 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 1e9e76a629d..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 @@ -272,7 +272,7 @@ private void handleTaskResult(SaveToDiskResult taskResult, SaveRequest saveReque 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, instance); + 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/instancemanagement/InstancesDataService.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstancesDataService.kt index a25b2b25e7f..a6c2e7ed48a 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 @@ -121,7 +121,7 @@ class InstancesDataService( if (finalizedInstance == null) { result.copy(failureCount = result.failureCount + 1) } else { - instanceFinalized(projectId, form, instance) + instanceFinalized(projectId, form) result } } @@ -177,7 +177,7 @@ class InstancesDataService( } } - fun sendInstances(projectId: String, instanceIds: List? = null): Boolean { + fun sendInstances(projectId: String, formAutoSendOnly: Boolean = false): Boolean { val projectDependencyProvider = projectDependencyProviderFactory.create(projectId) @@ -193,9 +193,11 @@ class InstancesDataService( projectDependencyProvider.projectId ).withLock { acquiredLock: Boolean -> if (acquiredLock) { - val toUpload = if (instanceIds != null) { - instanceIds.map { - projectDependencyProvider.instancesRepository.get(it)!! + val toUpload = if (formAutoSendOnly) { + projectDependencyProvider.instancesRepository.all.filter { + projectDependencyProvider.formsRepository.getLatestByFormIdAndVersion(it.formId, it.formVersion)?.let { form -> + form.autoSend != null && form.autoSend == "true" + } ?: false } } else { InstanceAutoSendFetcher.getInstancesToAutoSend( @@ -219,11 +221,11 @@ class InstancesDataService( } } - fun instanceFinalized(projectId: String, form: Form, instance: Instance) { + fun instanceFinalized(projectId: String, form: Form) { if (form.autoSend != null && form.autoSend == "true") { - instanceSubmitScheduler.scheduleSubmit(projectId, instance.dbId) + instanceSubmitScheduler.scheduleFormAutoSend(projectId) } else { - instanceSubmitScheduler.scheduleSubmitIfNeeded(projectId) + instanceSubmitScheduler.scheduleAutoSend(projectId) } } 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 87f95b34223..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.scheduleSubmitIfNeeded(projectsDataService.getCurrentProject().getUuid()); + instanceSubmitScheduler.scheduleAutoSend(projectsDataService.getCurrentProject().getUuid()); } } 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 f80505eae3b..7b102fa8ab8 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 @@ -89,7 +89,7 @@ class FormUpdateAndInstanceSubmitSchedulerTest { .save(ProjectKeys.KEY_AUTOSEND, "wifi_and_cellular") val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) - manager.scheduleSubmitIfNeeded("myProject") + manager.scheduleAutoSend("myProject") verify(scheduler).networkDeferred( eq("AutoSendWorker:myProject"), any(), @@ -104,7 +104,7 @@ class FormUpdateAndInstanceSubmitSchedulerTest { .save(ProjectKeys.KEY_AUTOSEND, "wifi_only") val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) - manager.scheduleSubmitIfNeeded("myProject") + manager.scheduleAutoSend("myProject") verify(scheduler).networkDeferred( eq("AutoSendWorker:myProject"), any(), @@ -119,7 +119,7 @@ class FormUpdateAndInstanceSubmitSchedulerTest { .save(ProjectKeys.KEY_AUTOSEND, "cellular_only") val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) - manager.scheduleSubmitIfNeeded("myProject") + manager.scheduleAutoSend("myProject") verify(scheduler).networkDeferred( eq("AutoSendWorker:myProject"), any(), @@ -134,7 +134,7 @@ class FormUpdateAndInstanceSubmitSchedulerTest { .save(ProjectKeys.KEY_AUTOSEND, "off") val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) - manager.scheduleSubmitIfNeeded("myProject") + manager.scheduleAutoSend("myProject") verifyNoInteractions(scheduler) } 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 039809ae089..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).scheduleSubmitIfNeeded(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()).scheduleSubmitIfNeeded(projectID) + verify(instanceSubmitScheduler, never()).scheduleAutoSend(projectID) } } From b4d5fdeebded112ab1204905cb6a9a56588f1548 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 2 May 2024 10:46:09 +0100 Subject: [PATCH 582/750] Make sure form level auto send doesn't resend forms --- .../backgroundwork/SendFormsTaskSpec.kt | 2 +- .../InstancesDataService.kt | 19 +++----- .../autosend/InstanceAutoSendFetcher.kt | 15 +++++-- .../autosend/InstanceAutoSendFetcherTest.kt | 44 +++++++++++++++++++ 4 files changed, 62 insertions(+), 18 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt index 1a40373750e..3b507577154 100644 --- a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt +++ b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt @@ -38,7 +38,7 @@ class SendFormsTaskSpec : TaskSpec { val formAutoSend = inputData[TaskData.DATA_FORM_AUTO_SEND] != null if (projectId != null) { if (formAutoSend) { - instancesDataService.sendInstances(projectId, formAutoSendOnly = true) + instancesDataService.sendInstances(projectId, formAutoSend = true) } else { instancesDataService.sendInstances(projectId) } 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 a6c2e7ed48a..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 @@ -177,7 +177,7 @@ class InstancesDataService( } } - fun sendInstances(projectId: String, formAutoSendOnly: Boolean = false): Boolean { + fun sendInstances(projectId: String, formAutoSend: Boolean = false): Boolean { val projectDependencyProvider = projectDependencyProviderFactory.create(projectId) @@ -193,18 +193,11 @@ class InstancesDataService( projectDependencyProvider.projectId ).withLock { acquiredLock: Boolean -> if (acquiredLock) { - val toUpload = if (formAutoSendOnly) { - projectDependencyProvider.instancesRepository.all.filter { - projectDependencyProvider.formsRepository.getLatestByFormIdAndVersion(it.formId, it.formVersion)?.let { form -> - form.autoSend != null && form.autoSend == "true" - } ?: false - } - } else { - InstanceAutoSendFetcher.getInstancesToAutoSend( - projectDependencyProvider.instancesRepository, - projectDependencyProvider.formsRepository - ) - } + val toUpload = InstanceAutoSendFetcher.getInstancesToAutoSend( + projectDependencyProvider.instancesRepository, + projectDependencyProvider.formsRepository, + formAutoSend + ) if (toUpload.isNotEmpty()) { val results = instanceSubmitter.submitInstances(toUpload) 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 84b8c9619e1..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,5 +1,6 @@ package org.odk.collect.android.instancemanagement.autosend +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 @@ -8,17 +9,23 @@ object InstanceAutoSendFetcher { fun getInstancesToAutoSend( instancesRepository: InstancesRepository, - formsRepository: FormsRepository + formsRepository: FormsRepository, + formAutoSend: Boolean = false ): List { val allFinalizedForms = instancesRepository.getAllByStatus( Instance.STATUS_COMPLETE, Instance.STATUS_SUBMISSION_FAILED ) + 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.autoSend == null - } ?: false + formsRepository.getLatestByFormIdAndVersion(it.formId, it.formVersion) + ?.let { form -> filter(form) } ?: false } } } 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 0001dcba000..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 @@ -88,4 +88,48 @@ class InstanceAutoSendFetcherTest { ) ) } + + @Test + fun `return all finalized forms with autosend when formAutoSend is true`() { + formsRepository.save(formWithEnabledAutoSend) + formsRepository.save(formWithoutSpecifiedAutoSend) + formsRepository.save(formWithDisabledAutoSend) + formsRepository.save(formWithCustomAutoSend) + + instancesRepository.apply { + save(instanceOfFormWithEnabledAutoSendIncomplete) + save(instanceOfFormWithEnabledAutoSendComplete) + save(instanceOfFormWithEnabledAutoSendSubmissionFailed) + save(instanceOfFormWithEnabledAutoSendSubmitted) + + save(instanceOfFormWithoutSpecifiedAutoSendIncomplete) + save(instanceOfFormWithoutSpecifiedAutoSendComplete) + save(instanceOfFormWithoutSpecifiedAutoSendSubmissionFailed) + save(instanceOfFormWithoutSpecifiedAutoSendSubmitted) + + save(instanceOfFormWithDisabledAutoSendIncomplete) + save(instanceOfFormWithDisabledAutoSendComplete) + save(instanceOfFormWithDisabledAutoSendSubmissionFailed) + save(instanceOfFormWithDisabledAutoSendSubmitted) + + save(instanceOfFormWithCustomAutoSendIncomplete) + save(instanceOfFormWithCustomAutoSendComplete) + save(instanceOfFormWithCustomAutoSendSubmissionFailed) + save(instanceOfFormWithCustomAutoSendSubmitted) + } + + val instancesToSend = InstanceAutoSendFetcher.getInstancesToAutoSend( + instancesRepository, + formsRepository, + formAutoSend = true + ) + + assertThat( + instancesToSend.map { it.instanceFilePath }, + contains( + instanceOfFormWithEnabledAutoSendComplete.instanceFilePath, + instanceOfFormWithEnabledAutoSendSubmissionFailed.instanceFilePath, + ) + ) + } } From 6c8398a2ef53a3381fe4e8b4cf21348d7b01d130 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 3 May 2024 10:52:17 +0100 Subject: [PATCH 583/750] Simplify TaskSpec and make sure jobs are rescheduled to account for name changes etc --- .../async/CoroutineAndWorkManagerScheduler.kt | 16 +++-- .../java/org/odk/collect/async/TaskSpec.kt | 6 -- .../{WorkerAdapter.kt => TaskSpecWorker.kt} | 16 +++-- ...atesUpgrade.kt => ScheduledWorkUpgrade.kt} | 11 ++- .../upgrade/UpgradeInitializer.kt | 6 +- .../backgroundwork/AutoUpdateTaskSpec.kt | 9 --- .../backgroundwork/SendFormsTaskSpec.kt | 9 --- .../backgroundwork/SyncFormsTaskSpec.kt | 9 --- .../injection/config/AppDependencyModule.java | 10 +-- .../initialization/FormUpdatesUpgradeTest.kt | 35 ---------- .../ScheduledWorkUpgradeTest.kt | 67 +++++++++++++++++++ 11 files changed, 106 insertions(+), 88 deletions(-) rename async/src/main/java/org/odk/collect/async/{WorkerAdapter.kt => TaskSpecWorker.kt} (55%) rename collect_app/src/main/java/org/odk/collect/android/application/initialization/{FormUpdatesUpgrade.kt => ScheduledWorkUpgrade.kt} (57%) delete mode 100644 collect_app/src/test/java/org/odk/collect/android/application/initialization/FormUpdatesUpgradeTest.kt create mode 100644 collect_app/src/test/java/org/odk/collect/android/application/initialization/ScheduledWorkUpgradeTest.kt 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..9a067c6a375 100644 --- a/async/src/main/java/org/odk/collect/async/CoroutineAndWorkManagerScheduler.kt +++ b/async/src/main/java/org/odk/collect/async/CoroutineAndWorkManagerScheduler.kt @@ -28,10 +28,12 @@ class CoroutineAndWorkManagerScheduler(foregroundContext: CoroutineContext, back .setRequiredNetworkType(constraintNetworkType) .build() - val workManagerInputData = Data.Builder().putAll(inputData).build() + val workManagerInputData = Data.Builder() + .putString(TaskSpecWorker.TASK_SPEC_CLASS, spec.javaClass.name) + .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) @@ -45,10 +47,12 @@ class CoroutineAndWorkManagerScheduler(foregroundContext: CoroutineContext, back .setRequiredNetworkType(NetworkType.CONNECTED) .build() - val workManagerInputData = Data.Builder().putAll(inputData).build() + val workManagerInputData = Data.Builder() + .putString(TaskSpecWorker.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) 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/WorkerAdapter.kt b/async/src/main/java/org/odk/collect/async/TaskSpecWorker.kt similarity index 55% rename from async/src/main/java/org/odk/collect/async/WorkerAdapter.kt rename to async/src/main/java/org/odk/collect/async/TaskSpecWorker.kt index fc65d1575e0..26857fc2fe6 100644 --- a/async/src/main/java/org/odk/collect/async/WorkerAdapter.kt +++ b/async/src/main/java/org/odk/collect/async/TaskSpecWorker.kt @@ -4,15 +4,18 @@ import android.content.Context import androidx.work.Worker import androidx.work.WorkerParameters -abstract class WorkerAdapter( - private val spec: TaskSpec, +class TaskSpecWorker( context: Context, workerParams: WorkerParameters ) : Worker(context, workerParams) { override fun doWork(): Result { + val specClass = inputData.getString(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()).get() + val completed = + spec.getTask(applicationContext, stringInputData, isLastUniqueExecution(spec)).get() val maxRetries = spec.maxRetries return if (completed) { @@ -24,5 +27,10 @@ abstract class WorkerAdapter( } } - private fun isLastUniqueExecution() = spec.maxRetries?.let { runAttemptCount >= it } ?: true + private fun isLastUniqueExecution(spec: TaskSpec) = + spec.maxRetries?.let { runAttemptCount >= it } ?: true + + companion object { + const val TASK_SPEC_CLASS = "taskSpecClass" + } } 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/SendFormsTaskSpec.kt b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt index 3b507577154..f4672ceafdd 100644 --- a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt +++ b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt @@ -15,11 +15,9 @@ 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 @@ -47,11 +45,4 @@ class SendFormsTaskSpec : TaskSpec { } } } - - override fun getWorkManagerAdapter(): Class { - return Adapter::class.java - } - - class Adapter(context: Context, workerParams: WorkerParameters) : - WorkerAdapter(SendFormsTaskSpec(), 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/injection/config/AppDependencyModule.java b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java index dcc7ead4828..cdd6adc4dfa 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; @@ -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/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) + } +} From 4d8c8410e3af08f99bdea7a03bd958b0ba33a950 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 3 May 2024 10:58:19 +0100 Subject: [PATCH 584/750] Simplify NetworkStateProvider API --- .../network/ConnectivityProvider.kt | 19 +++++++++---------- .../network/NetworkStateProvider.kt | 6 +++++- .../async/CoroutineAndWorkManagerScheduler.kt | 2 +- .../java/org/odk/collect/async/Scheduler.kt | 3 ++- 4 files changed, 17 insertions(+), 13 deletions(-) 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 index 020dd81848b..783966df725 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/network/ConnectivityProvider.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/network/ConnectivityProvider.kt @@ -5,18 +5,17 @@ import android.net.ConnectivityManager import org.odk.collect.async.Scheduler class ConnectivityProvider(private val context: Context) : NetworkStateProvider { - override val isDeviceOnline: Boolean - get() { - val activeNetworkInfo = connectivityManager.activeNetworkInfo - return activeNetworkInfo != null && activeNetworkInfo.isConnected - } - override val currentNetwork: Scheduler.NetworkType? get() { - return when (connectivityManager.activeNetworkInfo?.type) { - ConnectivityManager.TYPE_WIFI -> Scheduler.NetworkType.WIFI - ConnectivityManager.TYPE_MOBILE -> Scheduler.NetworkType.CELLULAR - else -> null + return if (connectivityManager.activeNetworkInfo?.isConnected == true) { + when (connectivityManager.activeNetworkInfo?.type) { + ConnectivityManager.TYPE_WIFI -> Scheduler.NetworkType.WIFI + ConnectivityManager.TYPE_MOBILE -> Scheduler.NetworkType.CELLULAR + null -> null + else -> Scheduler.NetworkType.OTHER + } + } else { + null } } 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 index 8a2b4db3a8f..d775213f8b9 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/network/NetworkStateProvider.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/network/NetworkStateProvider.kt @@ -3,6 +3,10 @@ package org.odk.collect.androidshared.network import org.odk.collect.async.Scheduler interface NetworkStateProvider { - val isDeviceOnline: Boolean val currentNetwork: Scheduler.NetworkType? + + val isDeviceOnline: Boolean + get() { + return currentNetwork != null + } } 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 9a067c6a375..45179cf962c 100644 --- a/async/src/main/java/org/odk/collect/async/CoroutineAndWorkManagerScheduler.kt +++ b/async/src/main/java/org/odk/collect/async/CoroutineAndWorkManagerScheduler.kt @@ -21,7 +21,7 @@ class CoroutineAndWorkManagerScheduler(foregroundContext: CoroutineContext, back val constraintNetworkType = when (networkConstraint) { Scheduler.NetworkType.WIFI -> NetworkType.UNMETERED Scheduler.NetworkType.CELLULAR -> NetworkType.METERED - null -> NetworkType.CONNECTED + else -> NetworkType.CONNECTED } val constraints = Constraints.Builder() 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 } } From 1d88a3b68770f870b221c3be1df12587df43e9cd Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 3 May 2024 11:23:01 +0100 Subject: [PATCH 585/750] Retry cellular tasks if connected to a metered non-cellular connection --- async/src/main/AndroidManifest.xml | 3 +- .../async/CoroutineAndWorkManagerScheduler.kt | 50 +++++++++++++++---- .../org/odk/collect/async/TaskSpecWorker.kt | 13 ++++- .../async}/network/ConnectivityProvider.kt | 2 +- .../async}/network/NetworkStateProvider.kt | 2 +- .../support/FakeNetworkStateProvider.kt | 7 +-- .../collect/android/support/TestScheduler.kt | 2 +- .../activities/FormDownloadListActivity.java | 2 +- .../collect/android/application/Collect.java | 2 +- .../backgroundwork/SendFormsTaskSpec.kt | 2 +- .../blankformlist/BlankFormListActivity.kt | 2 +- .../BlankFormListMenuProvider.kt | 2 +- .../config/AppDependencyComponent.java | 2 +- .../injection/config/AppDependencyModule.java | 4 +- .../autosend/AutoSendSettingsProvider.kt | 2 +- .../send/InstanceUploaderListActivity.java | 2 +- .../BlankFormListMenuProviderTest.kt | 2 +- .../autosend/AutoSendSettingsProviderTest.kt | 2 +- 18 files changed, 69 insertions(+), 34 deletions(-) rename {androidshared/src/main/java/org/odk/collect/androidshared => async/src/main/java/org/odk/collect/async}/network/ConnectivityProvider.kt (95%) rename {androidshared/src/main/java/org/odk/collect/androidshared => async/src/main/java/org/odk/collect/async}/network/NetworkStateProvider.kt (82%) 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 45179cf962c..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,11 +13,24 @@ 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 @@ -29,7 +42,11 @@ class CoroutineAndWorkManagerScheduler(foregroundContext: CoroutineContext, back .build() val workManagerInputData = Data.Builder() - .putString(TaskSpecWorker.TASK_SPEC_CLASS, spec.javaClass.name) + .putString(TaskSpecWorker.DATA_TASK_SPEC_CLASS, spec.javaClass.name) + .putBoolean( + TaskSpecWorker.DATA_CELLULAR_ONLY, + networkConstraint == Scheduler.NetworkType.CELLULAR + ) .putAll(inputData) .build() @@ -42,17 +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() - .putString(TaskSpecWorker.TASK_SPEC_CLASS, spec.javaClass.name) + .putString(TaskSpecWorker.DATA_TASK_SPEC_CLASS, spec.javaClass.name) .putAll(inputData) .build() - val builder = PeriodicWorkRequest.Builder(TaskSpecWorker::class.java, repeatPeriod, TimeUnit.MILLISECONDS) + val builder = PeriodicWorkRequest.Builder( + TaskSpecWorker::class.java, + repeatPeriod, + TimeUnit.MILLISECONDS + ) .addTag(tag) .setInputData(workManagerInputData) .setConstraints(constraints) @@ -63,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/TaskSpecWorker.kt b/async/src/main/java/org/odk/collect/async/TaskSpecWorker.kt index 26857fc2fe6..873f88be48a 100644 --- a/async/src/main/java/org/odk/collect/async/TaskSpecWorker.kt +++ b/async/src/main/java/org/odk/collect/async/TaskSpecWorker.kt @@ -3,14 +3,22 @@ package org.odk.collect.async import android.content.Context import androidx.work.Worker import androidx.work.WorkerParameters +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 specClass = inputData.getString(TASK_SPEC_CLASS)!! + val cellularOnly = inputData.getBoolean(DATA_CELLULAR_ONLY, false) + if (cellularOnly && connectivityProvider.currentNetwork != Scheduler.NetworkType.CELLULAR) { + 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() } @@ -31,6 +39,7 @@ class TaskSpecWorker( spec.maxRetries?.let { runAttemptCount >= it } ?: true companion object { - const val TASK_SPEC_CLASS = "taskSpecClass" + const val DATA_TASK_SPEC_CLASS = "taskSpecClass" + const val DATA_CELLULAR_ONLY = "cellularOnly" } } diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/network/ConnectivityProvider.kt b/async/src/main/java/org/odk/collect/async/network/ConnectivityProvider.kt similarity index 95% rename from androidshared/src/main/java/org/odk/collect/androidshared/network/ConnectivityProvider.kt rename to async/src/main/java/org/odk/collect/async/network/ConnectivityProvider.kt index 783966df725..a789e868c90 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/network/ConnectivityProvider.kt +++ b/async/src/main/java/org/odk/collect/async/network/ConnectivityProvider.kt @@ -1,4 +1,4 @@ -package org.odk.collect.androidshared.network +package org.odk.collect.async.network import android.content.Context import android.net.ConnectivityManager diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/network/NetworkStateProvider.kt b/async/src/main/java/org/odk/collect/async/network/NetworkStateProvider.kt similarity index 82% rename from androidshared/src/main/java/org/odk/collect/androidshared/network/NetworkStateProvider.kt rename to async/src/main/java/org/odk/collect/async/network/NetworkStateProvider.kt index d775213f8b9..410454a5106 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/network/NetworkStateProvider.kt +++ b/async/src/main/java/org/odk/collect/async/network/NetworkStateProvider.kt @@ -1,4 +1,4 @@ -package org.odk.collect.androidshared.network +package org.odk.collect.async.network import org.odk.collect.async.Scheduler 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 index 2ac0de10d5d..395e59f4db8 100644 --- 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 @@ -1,25 +1,20 @@ package org.odk.collect.android.support -import org.odk.collect.androidshared.network.NetworkStateProvider import org.odk.collect.async.Scheduler +import org.odk.collect.async.network.NetworkStateProvider class FakeNetworkStateProvider : NetworkStateProvider { - private var online = true private var type: Scheduler.NetworkType? = Scheduler.NetworkType.WIFI fun goOnline(networkType: Scheduler.NetworkType) { - online = true type = networkType } fun goOffline() { - online = false type = null } - override val isDeviceOnline: Boolean - get() = online override val currentNetwork: Scheduler.NetworkType? get() = type } 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 1fb4f4f1773..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 @@ -7,11 +7,11 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Runnable import kotlinx.coroutines.flow.Flow -import org.odk.collect.androidshared.network.NetworkStateProvider 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 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/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/backgroundwork/SendFormsTaskSpec.kt b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt index f4672ceafdd..6cedcb5aeb9 100644 --- a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt +++ b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt @@ -25,7 +25,7 @@ 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 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 5ca03856628..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 @@ -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; 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 cdd6adc4dfa..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 @@ -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; 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 837579b282d..c8997f40c08 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,7 +1,7 @@ package org.odk.collect.android.instancemanagement.autosend import android.app.Application -import org.odk.collect.androidshared.network.NetworkStateProvider +import org.odk.collect.async.network.NetworkStateProvider import org.odk.collect.async.Scheduler import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.enums.AutoSend 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/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/autosend/AutoSendSettingsProviderTest.kt b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/AutoSendSettingsProviderTest.kt index 4a60a6e1b69..e3f13150a56 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 @@ -9,7 +9,7 @@ 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.network.NetworkStateProvider import org.odk.collect.async.Scheduler import org.odk.collect.projects.Project import org.odk.collect.settings.InMemSettingsProvider From 8b1f171e03cc7603247074c2b3f6334d0eb2f50b Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 3 May 2024 11:45:22 +0100 Subject: [PATCH 586/750] Rework test --- ...erAdapterTest.kt => TaskSpecWorkerTest.kt} | 119 ++++++++++++------ .../autosend/AutoSendSettingsProvider.kt | 2 +- .../autosend/AutoSendSettingsProviderTest.kt | 2 +- 3 files changed, 81 insertions(+), 42 deletions(-) rename async/src/test/java/org/odk/collect/async/{WorkerAdapterTest.kt => TaskSpecWorkerTest.kt} (51%) 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..b28f0a2dbcd 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)) +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/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 c8997f40c08..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 org.odk.collect.async.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 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 e3f13150a56..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 @@ -9,8 +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.async.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 From 8ca2b49da50e72d23a1798b755bd3a2d60a563c3 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 3 May 2024 12:03:34 +0100 Subject: [PATCH 587/750] Remove test mirroring implementation --- .../collect/android/backgroundwork/SendFormsTaskSpecTest.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/collect_app/src/test/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpecTest.kt b/collect_app/src/test/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpecTest.kt index 99f3d0da418..915ef9b1192 100644 --- a/collect_app/src/test/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpecTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpecTest.kt @@ -47,11 +47,6 @@ class SendFormsTaskSpecTest { projectId = CollectHelpers.setupDemoProject() } - @Test - fun `maxRetries should not be limited`() { - assertThat(SendFormsTaskSpec().maxRetries, equalTo(null)) - } - @Test fun `returns false if sending instances fails`() { whenever(instancesDataService.sendInstances(projectId)).doReturn(false) From 57c459cdf0000419c0cf4eb54326789259a7a909 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 7 May 2024 17:37:35 +0100 Subject: [PATCH 588/750] Make sure network state provider is shared properly --- .../org/odk/collect/android/support/TestDependencies.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 cccc4760708..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,6 +12,7 @@ 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 { @@ -35,4 +37,9 @@ public Scheduler providesScheduler(WorkManager workManager) { public BarcodeViewDecoder providesBarcodeViewDecoder() { return stubBarcodeViewDecoder; } + + @Override + public NetworkStateProvider providesNetworkStateProvider(Context context) { + return networkStateProvider; + } } From 78a1bda04c6c1e412514199605118bdfa4023cbd Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 7 May 2024 17:55:43 +0100 Subject: [PATCH 589/750] Set user property when user runs into metered wifi --- .../src/main/java/org/odk/collect/analytics/Analytics.kt | 4 ++++ async/build.gradle.kts | 3 +++ async/src/main/java/org/odk/collect/async/TaskSpecWorker.kt | 2 ++ 3 files changed, 9 insertions(+) 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/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/java/org/odk/collect/async/TaskSpecWorker.kt b/async/src/main/java/org/odk/collect/async/TaskSpecWorker.kt index 873f88be48a..c3e41880436 100644 --- a/async/src/main/java/org/odk/collect/async/TaskSpecWorker.kt +++ b/async/src/main/java/org/odk/collect/async/TaskSpecWorker.kt @@ -3,6 +3,7 @@ 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( @@ -15,6 +16,7 @@ class TaskSpecWorker( 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() } From 8dd91aa83d38a7c1482dae8cdd4900e685089991 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 10 Jun 2024 11:10:49 +0100 Subject: [PATCH 590/750] Remove unneeded line from test --- .../collect/android/feature/instancemanagement/AutoSendTest.kt | 2 -- 1 file changed, 2 deletions(-) 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 67427bb3707..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 @@ -177,8 +177,6 @@ class AutoSendTest { @Test fun whenFormHasAutoSendDisabled_fillingAndFinalizingForm_doesNotSendForm_regardlessOfSetting() { - testDependencies.server.alwaysReturnError() - val mainMenuPage = rule.startAtMainMenu() .setServer(testDependencies.server.url) .enableAutoSend( From 5a900990cbe6e01cc6bbb842f17bd3813d5c8451 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 10 Jun 2024 11:15:19 +0100 Subject: [PATCH 591/750] Fix import --- mapbox/build.gradle.kts | 1 + .../java/org/odk/collect/mapbox/MapBoxInitializationFragment.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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 From f08bf48c2a582d7c163f71b1958a1384686fbfa8 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 10 Jun 2024 11:24:13 +0100 Subject: [PATCH 592/750] Update test names --- .../FormUpdateAndInstanceSubmitSchedulerTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 7b102fa8ab8..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 @@ -84,7 +84,7 @@ 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) @@ -99,7 +99,7 @@ class FormUpdateAndInstanceSubmitSchedulerTest { } @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) @@ -114,7 +114,7 @@ class FormUpdateAndInstanceSubmitSchedulerTest { } @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) @@ -129,7 +129,7 @@ class FormUpdateAndInstanceSubmitSchedulerTest { } @Test - fun `scheduleSubmit does nothing if auto send is disabled`() { + fun `scheduleAutoSend does nothing if auto send is disabled`() { settingsProvider.getUnprotectedSettings("myProject") .save(ProjectKeys.KEY_AUTOSEND, "off") val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) From 52cc9b15c3d11cc186275d48c0e31e89a259bb96 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 10 Jun 2024 11:27:53 +0100 Subject: [PATCH 593/750] Remove impossible when clause --- .../java/org/odk/collect/async/network/ConnectivityProvider.kt | 1 - 1 file changed, 1 deletion(-) 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 index a789e868c90..526ad50c363 100644 --- a/async/src/main/java/org/odk/collect/async/network/ConnectivityProvider.kt +++ b/async/src/main/java/org/odk/collect/async/network/ConnectivityProvider.kt @@ -11,7 +11,6 @@ class ConnectivityProvider(private val context: Context) : NetworkStateProvider when (connectivityManager.activeNetworkInfo?.type) { ConnectivityManager.TYPE_WIFI -> Scheduler.NetworkType.WIFI ConnectivityManager.TYPE_MOBILE -> Scheduler.NetworkType.CELLULAR - null -> null else -> Scheduler.NetworkType.OTHER } } else { From 64355c8a17d05832702b2b77bebe3a36d82dbe2d Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 10 Jun 2024 11:35:17 +0100 Subject: [PATCH 594/750] Make test helper private --- async/src/test/java/org/odk/collect/async/TaskSpecWorkerTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/async/src/test/java/org/odk/collect/async/TaskSpecWorkerTest.kt b/async/src/test/java/org/odk/collect/async/TaskSpecWorkerTest.kt index b28f0a2dbcd..df69f12ee0f 100644 --- a/async/src/test/java/org/odk/collect/async/TaskSpecWorkerTest.kt +++ b/async/src/test/java/org/odk/collect/async/TaskSpecWorkerTest.kt @@ -114,7 +114,7 @@ class TaskSpecWorkerTest { } } -class TestTaskSpec : TaskSpec { +private class TestTaskSpec : TaskSpec { companion object { From 59ebae7f56b2d2225d7202f6da30a516e4d138da Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 7 Jun 2024 16:28:31 +0200 Subject: [PATCH 595/750] Implemented deleting layers --- .../androidshared/ui/GroupClickListener.kt | 14 ++-- .../maps/layers/OfflineMapLayersPicker.kt | 39 ++++++++-- .../layers/OfflineMapLayersPickerAdapter.kt | 78 ++++++++++++++----- .../maps/layers/OfflineMapLayersViewModel.kt | 48 +++++++++--- .../layout/offline_map_layers_picker_item.xml | 7 +- strings/src/main/res/values/strings.xml | 3 + 6 files changed, 143 insertions(+), 46 deletions(-) diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/ui/GroupClickListener.kt b/androidshared/src/main/java/org/odk/collect/androidshared/ui/GroupClickListener.kt index 358b13a3084..2a2017dd79d 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/ui/GroupClickListener.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/ui/GroupClickListener.kt @@ -4,10 +4,14 @@ import android.view.View import androidx.constraintlayout.widget.Group // https://stackoverflow.com/questions/59020818/group-multiple-views-in-constraint-layout-to-set-only-one-click-listener -object GroupClickListener { - fun Group.addOnClickListener(listener: (view: View) -> Unit) { - referencedIds.forEach { id -> - rootView.findViewById(id).setOnClickListener(listener) - } +fun Group.addOnClickListener(listener: (view: View) -> Unit) { + referencedIds.forEach { id -> + rootView.findViewById(id).setOnClickListener(listener) + } +} + +fun List.addOnClickListener(listener: (view: View) -> Unit) { + forEach { + it.setOnClickListener(listener) } } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 360b692c5c7..9e72e020560 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -12,12 +12,14 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.odk.collect.androidshared.ui.DialogFragmentUtils import org.odk.collect.androidshared.ui.FragmentFactoryBuilder -import org.odk.collect.androidshared.ui.GroupClickListener.addOnClickListener +import org.odk.collect.androidshared.ui.addOnClickListener import org.odk.collect.async.Scheduler import org.odk.collect.maps.databinding.OfflineMapLayersPickerBinding import org.odk.collect.settings.SettingsProvider +import org.odk.collect.strings.localization.getLocalizedString import org.odk.collect.webpage.ExternalWebPageHelper class OfflineMapLayersPicker( @@ -26,7 +28,8 @@ class OfflineMapLayersPicker( private val scheduler: Scheduler, private val settingsProvider: SettingsProvider, private val externalWebPageHelper: ExternalWebPageHelper -) : BottomSheetDialogFragment() { +) : BottomSheetDialogFragment(), + OfflineMapLayersPickerAdapter.OfflineMapLayersPickerAdapterInterface { private val viewModel: OfflineMapLayersViewModel by activityViewModels { object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @@ -80,7 +83,7 @@ class OfflineMapLayersPicker( } binding.save.setOnClickListener { - viewModel.saveSelectedLayer() + viewModel.saveCheckedLayer() dismiss() } @@ -102,11 +105,10 @@ class OfflineMapLayersPicker( } } + val adapter = OfflineMapLayersPickerAdapter(this) + binding.layers.setAdapter(adapter) viewModel.existingLayers.observe(this) { layers -> - val adapter = OfflineMapLayersPickerAdapter(layers.first, layers.second) { - viewModel.changeSelectedLayerId(it) - } - binding.layers.setAdapter(adapter) + adapter.setData(layers) } } @@ -120,4 +122,27 @@ class OfflineMapLayersPicker( // ignore } } + + override fun onLayerChecked(layerId: String?) { + viewModel.onLayerChecked(layerId) + } + + override fun onLayerToggled(layerId: String?) { + viewModel.onLayerToggled(layerId) + } + + override fun onDeleteLayer(layerItem: CheckableReferenceLayer) { + MaterialAlertDialogBuilder(requireActivity()) + .setMessage(requireActivity().getLocalizedString(org.odk.collect.strings.R.string.delete_layer_confirmation_message, layerItem.name)) + .setPositiveButton(org.odk.collect.strings.R.string.delete_layer) { _, _ -> + layerItem.file?.delete() + if (layerItem.id == viewModel.getCheckedLayer()) { + viewModel.onLayerChecked(null) + } + viewModel.onLayerDeleted(layerItem.id) + } + .setNegativeButton(org.odk.collect.strings.R.string.cancel, null) + .create() + .show() + } } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt index e4be8ce6bdb..d0ca54e9749 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt @@ -1,16 +1,35 @@ package org.odk.collect.maps.layers import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import org.odk.collect.androidshared.ui.addOnClickListener import org.odk.collect.maps.databinding.OfflineMapLayersPickerItemBinding import org.odk.collect.strings.localization.getLocalizedString class OfflineMapLayersPickerAdapter( - private val layers: List, - private var selectedLayerId: String?, - private val onSelectedLayerChanged: (String?) -> Unit + private val listener: OfflineMapLayersPickerAdapterInterface ) : RecyclerView.Adapter() { + interface OfflineMapLayersPickerAdapterInterface { + fun onLayerChecked(layerId: String?) + fun onLayerToggled(layerId: String?) + fun onDeleteLayer(layerItem: CheckableReferenceLayer) + } + + private val diffUtil = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CheckableReferenceLayer, newItem: CheckableReferenceLayer): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: CheckableReferenceLayer, newItem: CheckableReferenceLayer): Boolean { + return oldItem == newItem + } + } + private val asyncListDiffer = AsyncListDiffer(this, diffUtil) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val binding = OfflineMapLayersPickerItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) @@ -18,28 +37,47 @@ class OfflineMapLayersPickerAdapter( } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.binding.radioButton.setChecked(false) - if (position == 0) { - holder.binding.radioButton.text = holder.binding.root.context.getLocalizedString(org.odk.collect.strings.R.string.none) - if (selectedLayerId == null) { - holder.binding.radioButton.setChecked(true) - } + val layer = asyncListDiffer.currentList[position] + + holder.binding.radioButton.setChecked(layer.isChecked) + + if (layer.id == null) { + holder.binding.title.text = holder.binding.root.context.getLocalizedString(org.odk.collect.strings.R.string.none) + holder.binding.arrow.visibility = View.GONE + } else { + holder.binding.title.text = layer.name + holder.binding.path.text = layer.file?.absolutePath + holder.binding.arrow.visibility = View.VISIBLE + } + + if (layer.isExpanded) { + holder.binding.arrow.setImageDrawable(ContextCompat.getDrawable(holder.binding.root.context, org.odk.collect.icons.R.drawable.ic_baseline_collapse_24)) + holder.binding.path.visibility = View.VISIBLE + holder.binding.deleteLayer.visibility = View.VISIBLE } else { - holder.binding.radioButton.text = layers[position - 1].name - if (selectedLayerId == layers[position - 1].id) { - holder.binding.radioButton.setChecked(true) - } + holder.binding.arrow.setImageDrawable(ContextCompat.getDrawable(holder.binding.root.context, org.odk.collect.icons.R.drawable.ic_baseline_expand_24)) + holder.binding.path.visibility = View.GONE + holder.binding.deleteLayer.visibility = View.GONE } - holder.binding.radioButton.setOnClickListener { - if (position == 0) { - onSelectedLayerChanged(null) - } else { - onSelectedLayerChanged(layers[position - 1].id) - } + + listOf(holder.binding.radioButton, holder.binding.title, holder.binding.path).addOnClickListener { + listener.onLayerChecked(layer.id) + } + + holder.binding.arrow.setOnClickListener { + listener.onLayerToggled(layer.id) + } + + holder.binding.deleteLayer.setOnClickListener { + listener.onDeleteLayer(layer) } } - override fun getItemCount() = layers.size + 1 + override fun getItemCount() = asyncListDiffer.currentList.size + + fun setData(layers: List) { + asyncListDiffer.submitList(layers) + } class ViewHolder(val binding: OfflineMapLayersPickerItemBinding) : RecyclerView.ViewHolder(binding.root) } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index f23e7bc6871..31bdbeaef0d 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -22,8 +22,8 @@ class OfflineMapLayersViewModel( private val _isLoading = MutableLiveData() val isLoading: LiveData = _isLoading - private val _existingLayers = MutableLiveData, String?>>() - val existingLayers: LiveData, String?>> = _existingLayers + private val _existingLayers = MutableLiveData>() + val existingLayers: LiveData> = _existingLayers private val _layersToImport = MutableLiveData>() val layersToImport: LiveData> = _layersToImport @@ -39,11 +39,13 @@ class OfflineMapLayersViewModel( scheduler.immediate( background = { val layers = referenceLayerRepository.getAll() - val selectedLayerId = + val checkedLayerId = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) + val newData = mutableListOf(CheckableReferenceLayer(null, null, "", checkedLayerId == null, false)) + newData.addAll(layers.map { CheckableReferenceLayer(it.id, it.file, it.name, it.id == checkedLayerId, false) }) _isLoading.postValue(false) - _existingLayers.postValue(Pair(layers, selectedLayerId)) + _existingLayers.postValue(newData) }, foreground = { } ) @@ -90,12 +92,40 @@ class OfflineMapLayersViewModel( ) } - fun saveSelectedLayer() { - val selectedLayerId = existingLayers.value?.second - settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, selectedLayerId) + fun saveCheckedLayer() { + settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, getCheckedLayer()) } - fun changeSelectedLayerId(selectedLayerId: String?) { - _existingLayers.postValue(_existingLayers.value?.copy(second = selectedLayerId)) + fun onLayerChecked(layerId: String?) { + _existingLayers.value = _existingLayers.value?.map { + it.copy(isChecked = it.id == layerId) + } + } + + fun onLayerToggled(layerId: String?) { + _existingLayers.value = _existingLayers.value?.map { + val isExpanded = if (it.id == layerId) { + !it.isExpanded + } else { + it.isExpanded + } + it.copy(isExpanded = isExpanded) + } + } + + fun onLayerDeleted(deletedLayerId: String?) { + _existingLayers.value = _existingLayers.value?.filter { it.id != deletedLayerId } + } + + fun getCheckedLayer(): String? { + return _existingLayers.value?.find { it.isChecked }?.id } } + +data class CheckableReferenceLayer( + val id: String?, + val file: File?, + val name: String, + val isChecked: Boolean, + val isExpanded: Boolean +) diff --git a/maps/src/main/res/layout/offline_map_layers_picker_item.xml b/maps/src/main/res/layout/offline_map_layers_picker_item.xml index 879cdc91cdc..37a1a8ce499 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker_item.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker_item.xml @@ -23,18 +23,17 @@ app:layout_constraintStart_toEndOf="@id/radio_button" app:layout_constraintEnd_toStartOf="@id/arrow" app:layout_constraintTop_toTopOf="@id/radio_button" + app:layout_constraintBottom_toBottomOf="@id/radio_button" tools:text="Layer1" /> + app:layout_constraintTop_toBottomOf="@id/path" /> Current project only + + Are you sure you want to delete %1$s offline layers? + Record a point Accuracy: %1$s m Location provider: %s From b3767d80aed3a790de32dcc70baa6b4a515a98e0 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 28 May 2024 00:26:35 +0200 Subject: [PATCH 596/750] Converted DrawableMatcher to kotlin and moved to the androidtest module --- .../collect/androidtest/DrawableMatcher.kt | 62 +++++++++++++++++ .../android/regression/AboutPageTest.java | 2 +- .../android/regression/FillBlankFormTest.java | 2 +- .../support/matchers/DrawableMatcher.java | 68 ------------------- .../android/support/pages/QRCodePage.java | 2 +- 5 files changed, 65 insertions(+), 71 deletions(-) create mode 100644 androidtest/src/main/java/org/odk/collect/androidtest/DrawableMatcher.kt delete mode 100644 collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/DrawableMatcher.java diff --git a/androidtest/src/main/java/org/odk/collect/androidtest/DrawableMatcher.kt b/androidtest/src/main/java/org/odk/collect/androidtest/DrawableMatcher.kt new file mode 100644 index 00000000000..e683f4cdbfa --- /dev/null +++ b/androidtest/src/main/java/org/odk/collect/androidtest/DrawableMatcher.kt @@ -0,0 +1,62 @@ +package org.odk.collect.androidtest + +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.os.StrictMode +import android.os.StrictMode.ThreadPolicy +import android.view.View +import android.widget.ImageView +import androidx.test.espresso.matcher.BoundedMatcher +import org.hamcrest.Description +import org.hamcrest.Matcher + +object DrawableMatcher { + @JvmStatic + fun withImageDrawable(expectedResourceId: Int): Matcher { + return object : BoundedMatcher(ImageView::class.java) { + override fun describeTo(description: Description) { + description.appendText("has image drawable resource $expectedResourceId") + } + + public override fun matchesSafely(imageView: ImageView): Boolean { + return expectedResourceId == imageView.tag as Int + } + } + } + + @JvmStatic + fun withBitmap(match: Bitmap?): Matcher { + return object : BoundedMatcher(ImageView::class.java) { + override fun describeTo(description: Description) { + description.appendText("bitmaps did not match") + } + + override fun matchesSafely(imageView: ImageView): Boolean { + val drawable = imageView.drawable + if (drawable == null && match == null) { + return true + } else if (drawable != null && match == null) { + return false + } else if (drawable == null) { + return false + } + + val actual = (drawable as BitmapDrawable).bitmap + + val originalThreadPolicy = StrictMode.getThreadPolicy() + + try { + // Permit slow calls to allow `sameAs` use + StrictMode.setThreadPolicy( + ThreadPolicy.Builder() + .permitCustomSlowCalls().build() + ) + + return actual.sameAs(match) + } finally { + StrictMode.setThreadPolicy(originalThreadPolicy) + } + } + } + } +} diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/regression/AboutPageTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/regression/AboutPageTest.java index b35f3a295da..e672ff46f61 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/regression/AboutPageTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/regression/AboutPageTest.java @@ -14,7 +14,7 @@ import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.withText; -import static org.odk.collect.android.support.matchers.DrawableMatcher.withImageDrawable; +import static org.odk.collect.androidtest.DrawableMatcher.withImageDrawable; import static org.odk.collect.testshared.RecyclerViewMatcher.withRecyclerView; //Issue NODK-234 diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java index a0a6bb76ef2..e78c81c311a 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java @@ -5,7 +5,7 @@ import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static junit.framework.TestCase.assertNotSame; -import static org.odk.collect.android.support.matchers.DrawableMatcher.withImageDrawable; +import static org.odk.collect.androidtest.DrawableMatcher.withImageDrawable; import static org.odk.collect.testshared.RecyclerViewMatcher.withRecyclerView; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/DrawableMatcher.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/DrawableMatcher.java deleted file mode 100644 index 521c6b3faea..00000000000 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/DrawableMatcher.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.odk.collect.android.support.matchers; - -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.os.StrictMode; -import android.view.View; -import android.widget.ImageView; - -import androidx.test.espresso.matcher.BoundedMatcher; - -import org.hamcrest.Description; -import org.hamcrest.Matcher; - -public final class DrawableMatcher { - - private DrawableMatcher() { - } - - public static Matcher withImageDrawable(final int expectedResourceId) { - return new BoundedMatcher(ImageView.class) { - @Override - public void describeTo(Description description) { - description.appendText("has image drawable resource " + expectedResourceId); - } - - @Override - public boolean matchesSafely(ImageView imageView) { - return expectedResourceId == (Integer) imageView.getTag(); - } - }; - } - - public static Matcher withBitmap(Bitmap match) { - return new BoundedMatcher<>(ImageView.class) { - @Override - public void describeTo(Description description) { - description.appendText("bitmaps did not match"); - } - - @Override - protected boolean matchesSafely(ImageView imageView) { - Drawable drawable = imageView.getDrawable(); - if (drawable == null && match == null) { - return true; - } else if (drawable != null && match == null) { - return false; - } else if (drawable == null && match != null) { - return false; - } - - Bitmap actual = ((BitmapDrawable) drawable).getBitmap(); - - StrictMode.ThreadPolicy originalThreadPolicy = StrictMode.getThreadPolicy(); - - try { - // Permit slow calls to allow `sameAs` use - StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() - .permitCustomSlowCalls().build()); - - return actual.sameAs(match); - } finally { - StrictMode.setThreadPolicy(originalThreadPolicy); - } - } - }; - } -} diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/QRCodePage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/QRCodePage.java index ba0a77a9f40..69b9d91d252 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/QRCodePage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/QRCodePage.java @@ -14,7 +14,7 @@ import org.odk.collect.android.support.ActivityHelpers; import org.odk.collect.android.support.WaitFor; -import org.odk.collect.android.support.matchers.DrawableMatcher; +import org.odk.collect.androidtest.DrawableMatcher; public class QRCodePage extends Page { @Override From 24be6522ec70ff098379f6487cbddcf87330f993 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 28 May 2024 00:58:44 +0200 Subject: [PATCH 597/750] Improved DrawableMatcher to check real drawables not tags --- .../java/org/odk/collect/androidtest/DrawableMatcher.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/androidtest/src/main/java/org/odk/collect/androidtest/DrawableMatcher.kt b/androidtest/src/main/java/org/odk/collect/androidtest/DrawableMatcher.kt index e683f4cdbfa..102c417b456 100644 --- a/androidtest/src/main/java/org/odk/collect/androidtest/DrawableMatcher.kt +++ b/androidtest/src/main/java/org/odk/collect/androidtest/DrawableMatcher.kt @@ -1,11 +1,16 @@ package org.odk.collect.androidtest +import android.app.Application import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.VectorDrawable import android.os.StrictMode import android.os.StrictMode.ThreadPolicy import android.view.View import android.widget.ImageView +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.drawable.toBitmap +import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.matcher.BoundedMatcher import org.hamcrest.Description import org.hamcrest.Matcher @@ -19,7 +24,8 @@ object DrawableMatcher { } public override fun matchesSafely(imageView: ImageView): Boolean { - return expectedResourceId == imageView.tag as Int + val context = ApplicationProvider.getApplicationContext() + return imageView.drawable.toBitmap().rowBytes == (AppCompatResources.getDrawable(context, expectedResourceId) as VectorDrawable).toBitmap().rowBytes } } } From 580735cb0a0019da0a2ab6d1eccfaf06e6e94e2b Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 7 Jun 2024 20:41:32 +0200 Subject: [PATCH 598/750] Added new tests --- maps/build.gradle.kts | 2 + .../maps/layers/OfflineMapLayersPickerTest.kt | 187 ++++++++++++++++-- 2 files changed, 168 insertions(+), 21 deletions(-) diff --git a/maps/build.gradle.kts b/maps/build.gradle.kts index 66c2fd3597b..627fa4906f7 100644 --- a/maps/build.gradle.kts +++ b/maps/build.gradle.kts @@ -64,11 +64,13 @@ dependencies { debugImplementation(project(":fragments-test")) + testImplementation(project(":androidtest")) testImplementation(project(":test-shared")) testImplementation(Dependencies.junit) testImplementation(Dependencies.androidx_test_ext_junit) testImplementation(Dependencies.hamcrest) testImplementation(Dependencies.robolectric) testImplementation(Dependencies.mockito_kotlin) + testImplementation(Dependencies.androidx_test_espresso_contrib) testImplementation(Dependencies.androidx_test_espresso_core) } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index cfff10481f9..ff44354bfb2 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -7,14 +7,19 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.core.app.ActivityOptionsCompat import androidx.core.net.toUri import androidx.fragment.app.testing.FragmentScenario +import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.scrollTo import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isEnabled +import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -30,6 +35,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.odk.collect.androidshared.ui.FragmentFactoryBuilder +import org.odk.collect.androidtest.DrawableMatcher.withImageDrawable import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule import org.odk.collect.maps.R import org.odk.collect.settings.InMemSettingsProvider @@ -73,7 +79,7 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'cancel' button dismisses the layers picker`() { - val scenario = launchOfflineMapLayersPicker() + val scenario = launchFragment() scenario.onFragment { assertThat(it.isVisible, equalTo(true)) @@ -88,7 +94,7 @@ class OfflineMapLayersPickerTest { listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) ) - launchOfflineMapLayersPicker() + launchFragment() scheduler.flush() @@ -98,14 +104,14 @@ class OfflineMapLayersPickerTest { @Test fun `the 'cancel' button should be enabled during loading layers`() { - launchOfflineMapLayersPicker() + launchFragment() onView(withText(string.cancel)).check(matches(isEnabled())) } @Test fun `clicking the 'save' button dismisses the layers picker`() { - val scenario = launchOfflineMapLayersPicker() + val scenario = launchFragment() scheduler.flush() @@ -118,7 +124,7 @@ class OfflineMapLayersPickerTest { @Test fun `the 'save' button should be disabled during loading layers`() { - launchOfflineMapLayersPicker() + launchFragment() onView(withText(string.save)).check(matches(not(isEnabled()))) scheduler.flush() @@ -131,7 +137,7 @@ class OfflineMapLayersPickerTest { listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) ) - launchOfflineMapLayersPicker() + launchFragment() scheduler.flush() @@ -145,7 +151,7 @@ class OfflineMapLayersPickerTest { listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) ) - launchOfflineMapLayersPicker() + launchFragment() scheduler.flush() @@ -160,7 +166,7 @@ class OfflineMapLayersPickerTest { listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) ) - launchOfflineMapLayersPicker() + launchFragment() scheduler.flush() @@ -179,7 +185,7 @@ class OfflineMapLayersPickerTest { settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, "2") - launchOfflineMapLayersPicker() + launchFragment() scheduler.flush() @@ -190,7 +196,7 @@ class OfflineMapLayersPickerTest { @Test fun `progress indicator is displayed during loading layers`() { - launchOfflineMapLayersPicker() + launchFragment() onView(withId(R.id.progress_indicator)).check(matches(isDisplayed())) onView(withId(R.id.layers)).check(matches(not(isDisplayed()))) @@ -203,14 +209,14 @@ class OfflineMapLayersPickerTest { @Test fun `the 'learn more' button should be enabled during loading layers`() { - launchOfflineMapLayersPicker() + launchFragment() onView(withText(string.get_help_with_reference_layers)).check(matches(isEnabled())) } @Test fun `clicking the 'learn more' button opens the forum thread`() { - launchOfflineMapLayersPicker() + launchFragment() scheduler.flush() @@ -221,7 +227,7 @@ class OfflineMapLayersPickerTest { @Test fun `if there are no layers the 'none' option is displayed`() { - launchOfflineMapLayersPicker() + launchFragment() scheduler.flush() @@ -238,7 +244,7 @@ class OfflineMapLayersPickerTest { ) ) - launchOfflineMapLayersPicker() + launchFragment() scheduler.flush() @@ -254,7 +260,7 @@ class OfflineMapLayersPickerTest { listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) ) - launchOfflineMapLayersPicker() + launchFragment() scheduler.flush() @@ -273,7 +279,7 @@ class OfflineMapLayersPickerTest { listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) ) - val scenario = launchOfflineMapLayersPicker() + val scenario = launchFragment() scheduler.flush() @@ -285,7 +291,7 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'add layer' and selecting layers displays the confirmation dialog`() { - val scenario = launchOfflineMapLayersPicker() + val scenario = launchFragment() uris.add(Uri.parse("blah")) EspressoHelpers.clickOnText(string.add_layer) @@ -300,7 +306,7 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'add layer' and selecting nothing does not display the confirmation dialog`() { - val scenario = launchOfflineMapLayersPicker() + val scenario = launchFragment() EspressoHelpers.clickOnText(string.add_layer) @@ -317,7 +323,7 @@ class OfflineMapLayersPickerTest { val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) - launchOfflineMapLayersPicker() + launchFragment() scheduler.flush() @@ -342,7 +348,7 @@ class OfflineMapLayersPickerTest { val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) - launchOfflineMapLayersPicker() + launchFragment() scheduler.flush() @@ -366,7 +372,146 @@ class OfflineMapLayersPickerTest { onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.radio_button)).check(matches(withText(file2.name))) } - private fun launchOfflineMapLayersPicker(): FragmentScenario { + @Test + fun `layers are collapsed by default`() { + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), + ReferenceLayer("2", TempFiles.createTempFile(), "layer2") + ) + ) + + launchFragment() + + scheduler.flush() + + assertLayerCollapsed(1) + assertLayerCollapsed(2) + } + + @Test + fun `recreating maintains expanded layers`() { + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), + ReferenceLayer("2", TempFiles.createTempFile(), "layer2"), + ReferenceLayer("3", TempFiles.createTempFile(), "layer3") + )) + + val scenario = launchFragment() + + scheduler.flush() + + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.arrow)).perform(click()) + onView(withId(R.id.layers)).perform(scrollToPosition(3)) + onView(withRecyclerView(R.id.layers).atPositionOnView(3, R.id.arrow)).perform(click()) + + scenario.recreate() + + assertLayerExpanded(1) + assertLayerCollapsed(2) + assertLayerExpanded(3) + } + + @Test + fun `correct path is displayed after expanding layers`() { + val file1 = TempFiles.createTempFile() + val file2 = TempFiles.createTempFile() + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + ReferenceLayer("1", file1, "layer1"), + ReferenceLayer("2", file2, "layer2") + )) + + launchFragment() + + scheduler.flush() + + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.arrow)).perform(click()) + onView(withId(R.id.layers)).perform(scrollToPosition(2)) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.arrow)).perform(click()) + + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.path)).check(matches(withText(file1.absolutePath))) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.path)).check(matches(withText(file2.absolutePath))) + } + + @Test + fun `clicking delete shows the confirmation dialog`() { + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + )) + + launchFragment() + + scheduler.flush() + + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.arrow)).perform(click()) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform(scrollTo(), click()) + + onView(withText(string.cancel)).inRoot(isDialog()).check(matches(isDisplayed())) + onView(withText(string.delete_layer)).inRoot(isDialog()).check(matches(isDisplayed())) + } + + @Test + fun `clicking delete and canceling does not remove the layer`() { + val layerFile1 = TempFiles.createTempFile() + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + ReferenceLayer("1", layerFile1, "layer1") + )) + + launchFragment() + + scheduler.flush() + + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.arrow)).perform(click()) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform(scrollTo(), click()) + + onView(withText(string.cancel)).inRoot(isDialog()).perform(click()) + + onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) + onView(withId(R.id.layers)).perform(scrollToPosition(0)) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.title)).check(matches(withText("layer1"))) + assertThat(layerFile1.exists(), equalTo(true)) + } + + @Test + fun `clicking delete and confirming removes the layer`() { + val layerFile1 = TempFiles.createTempFile() + val layerFile2 = TempFiles.createTempFile() + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + ReferenceLayer("1", layerFile1, "layer1"), + ReferenceLayer("2", layerFile2, "layer2") + )) + + launchFragment() + + scheduler.flush() + + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.arrow)).perform(click()) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform(scrollTo(), click()) + + onView(withText(string.delete_layer)).inRoot(isDialog()).perform(click()) + scheduler.flush() + + onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.title)).check(matches(withText("layer2"))) + assertThat(layerFile1.exists(), equalTo(false)) + assertThat(layerFile2.exists(), equalTo(true)) + } + + private fun assertLayerCollapsed(position: Int) { + onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.arrow)).check(matches(withImageDrawable(org.odk.collect.icons.R.drawable.ic_baseline_expand_24))) + onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.path)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) + onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.delete_layer)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) + } + + private fun assertLayerExpanded(position: Int) { + onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.arrow)).check(matches(withImageDrawable(org.odk.collect.icons.R.drawable.ic_baseline_collapse_24))) + onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.path)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.delete_layer)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + } + + private fun launchFragment(): FragmentScenario { return fragmentScenarioLauncherRule.launchInContainer(OfflineMapLayersPicker::class.java) } } From 05b672c98e0f1013f64c5cd8b5c81ef9b0137fe0 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 7 Jun 2024 20:59:58 +0200 Subject: [PATCH 599/750] Fixed ids --- .../maps/layers/OfflineMapLayersPickerTest.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index ff44354bfb2..3ae8b7c3be6 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -232,7 +232,7 @@ class OfflineMapLayersPickerTest { scheduler.flush() onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(1))) - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(withText(string.none))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) } @Test @@ -249,9 +249,9 @@ class OfflineMapLayersPickerTest { scheduler.flush() onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3))) - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(withText(string.none))) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(withText("layer1"))) - onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.radio_button)).check(matches(withText("layer2"))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.title)).check(matches(withText("layer1"))) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.title)).check(matches(withText("layer2"))) } @Test @@ -367,9 +367,9 @@ class OfflineMapLayersPickerTest { scheduler.flush() onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3))) - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(withText(string.none))) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(withText(file1.name))) - onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.radio_button)).check(matches(withText(file2.name))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.title)).check(matches(withText(file1.name))) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.title)).check(matches(withText(file2.name))) } @Test From 244858664cb88727c0fc484bea86257a6f9d55c7 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 10 Jun 2024 17:11:23 +0200 Subject: [PATCH 600/750] Do not ignore the strings used in the layers picker --- strings/src/main/res/values/strings.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 5fad0a77c73..e2b882d5d42 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -489,29 +489,29 @@ Select reference layer - Select the reference layer to use for all maps in this project. You can add options to the list by selecting an MBTile file from your device. + Select the reference layer to use for all maps in this project. You can add options to the list by selecting an MBTile file from your device. Learn more about adding MBTiles. - Add layer + Add layer - Delete layer + Delete layer - Layers + Layers - Select layer access - Do you want the layer available in all projects or your current project only? + Select layer access + Do you want the layer available in all projects or your current project only? - All projects + All projects - Current project only + Current project only Are you sure you want to delete %1$s offline layers? From 58ce8f8b81b334c6db786eb6ec1c92027cc2c64f Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 11 Jun 2024 11:59:31 +0100 Subject: [PATCH 601/750] Use highest surface container color for rank items --- collect_app/src/main/res/layout/ranking_item.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collect_app/src/main/res/layout/ranking_item.xml b/collect_app/src/main/res/layout/ranking_item.xml index 63c78c39fd5..7158694f35e 100644 --- a/collect_app/src/main/res/layout/ranking_item.xml +++ b/collect_app/src/main/res/layout/ranking_item.xml @@ -18,7 +18,7 @@ limitations under the License. android:layout_height="wrap_content" android:padding="10dp" android:layout_marginTop="5dp" - android:background="@color/colorSurfaceContainerLow"> + android:background="?colorSurfaceContainerHighest"> Date: Tue, 11 Jun 2024 14:21:21 +0100 Subject: [PATCH 602/750] Convert property to method --- .../savedformlist/SavedFormListViewModel.kt | 2 +- .../InstancesDataService.kt | 5 +++- .../SavedFormListViewModelTest.kt | 24 +++++++++---------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/SavedFormListViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/SavedFormListViewModel.kt index f9b826e329c..810036322f4 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/SavedFormListViewModel.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/SavedFormListViewModel.kt @@ -39,7 +39,7 @@ class SavedFormListViewModel( _filterText.value = value } - val formsToDisplay: LiveData> = instancesDataService.instances + val formsToDisplay: LiveData> = instancesDataService.getInstances(projectId) .map { instances -> instances.filter { instance -> instance.deletedDate == null } } .combine(_sortOrder) { instances, order -> when (order) { 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 310bda069c2..d11d3f34bfd 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 @@ -32,7 +32,10 @@ class InstancesDataService( 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) - val instances: Flow> = appState.getFlow("instances", emptyList()) + + fun getInstances(projectId: String): Flow> { + return appState.getFlow("instances", emptyList()) + } fun update(projectId: String) { val projectDependencyProvider = projectDependencyProviderFactory.create(projectId) diff --git a/collect_app/src/test/java/org/odk/collect/android/formlists/savedformlist/SavedFormListViewModelTest.kt b/collect_app/src/test/java/org/odk/collect/android/formlists/savedformlist/SavedFormListViewModelTest.kt index 7814ee1ec8c..7f0caafca86 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formlists/savedformlist/SavedFormListViewModelTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formlists/savedformlist/SavedFormListViewModelTest.kt @@ -31,14 +31,14 @@ class SavedFormListViewModelTest { private val settings = InMemSettings() private val instancesDataService: InstancesDataService = mock { - on { instances } doReturn MutableStateFlow(emptyList()) + on { getInstances(any()) } doReturn MutableStateFlow(emptyList()) } @Test fun `formsToDisplay should not include deleted forms`() { val myForm = InstanceFixtures.instance(displayName = "My form", deletedDate = 1) val yourForm = InstanceFixtures.instance(displayName = "Your form") - saveForms(listOf(myForm, yourForm)) + saveForms("projectId", listOf(myForm, yourForm)) val viewModel = SavedFormListViewModel(scheduler, settings, instancesDataService, "projectId") @@ -52,7 +52,7 @@ class SavedFormListViewModelTest { fun `setting filterText filters forms on display name`() { val myForm = InstanceFixtures.instance(displayName = "My form") val yourForm = InstanceFixtures.instance(displayName = "Your form") - saveForms(listOf(myForm, yourForm)) + saveForms("projectId", listOf(myForm, yourForm),) val viewModel = SavedFormListViewModel(scheduler, settings, instancesDataService, "projectId") @@ -74,7 +74,7 @@ class SavedFormListViewModelTest { fun `clearing filterText does not filter forms`() { val myForm = InstanceFixtures.instance(displayName = "My form") val yourForm = InstanceFixtures.instance(displayName = "Your form") - saveForms(listOf(myForm, yourForm)) + saveForms("projectId", listOf(myForm, yourForm),) val viewModel = SavedFormListViewModel(scheduler, settings, instancesDataService, "projectId") @@ -96,7 +96,7 @@ class SavedFormListViewModelTest { fun `filtering forms is not case sensitive`() { val myForm = InstanceFixtures.instance(displayName = "My form") val yourForm = InstanceFixtures.instance(displayName = "Your form") - saveForms(listOf(myForm, yourForm)) + saveForms("projectId", listOf(myForm, yourForm),) val viewModel = SavedFormListViewModel(scheduler, settings, instancesDataService, "projectId") @@ -112,7 +112,7 @@ class SavedFormListViewModelTest { fun `can sort forms by ascending name`() { val a = InstanceFixtures.instance(displayName = "A") val b = InstanceFixtures.instance(displayName = "B") - saveForms(listOf(b, a)) + saveForms("projectId", listOf(b, a),) val viewModel = SavedFormListViewModel(scheduler, settings, instancesDataService, "projectId") @@ -128,7 +128,7 @@ class SavedFormListViewModelTest { fun `can sort forms by descending name`() { val a = InstanceFixtures.instance(displayName = "A") val b = InstanceFixtures.instance(displayName = "B") - saveForms(listOf(a, b)) + saveForms("projectId", listOf(a, b),) val viewModel = SavedFormListViewModel(scheduler, settings, instancesDataService, "projectId") @@ -144,7 +144,7 @@ class SavedFormListViewModelTest { fun `can sort forms by descending date`() { val a = InstanceFixtures.instance(displayName = "A", lastStatusChangeDate = 0) val b = InstanceFixtures.instance(displayName = "B", lastStatusChangeDate = 1) - saveForms(listOf(a, b)) + saveForms("projectId", listOf(a, b),) val viewModel = SavedFormListViewModel(scheduler, settings, instancesDataService, "projectId") @@ -160,7 +160,7 @@ class SavedFormListViewModelTest { fun `can sort forms by ascending date`() { val a = InstanceFixtures.instance(displayName = "A", lastStatusChangeDate = 0) val b = InstanceFixtures.instance(displayName = "B", lastStatusChangeDate = 1) - saveForms(listOf(b, a)) + saveForms("projectId", listOf(b, a),) val viewModel = SavedFormListViewModel(scheduler, settings, instancesDataService, "projectId") @@ -176,7 +176,7 @@ class SavedFormListViewModelTest { fun `sort order is retained between view models`() { val a = InstanceFixtures.instance(displayName = "A", lastStatusChangeDate = 0) val b = InstanceFixtures.instance(displayName = "B", lastStatusChangeDate = 1) - saveForms(listOf(b, a)) + saveForms("projectId", listOf(b, a),) val viewModel = SavedFormListViewModel(scheduler, settings, instancesDataService, "projectId") @@ -224,7 +224,7 @@ class SavedFormListViewModelTest { assertThat(result.getOrAwaitValue(scheduler)!!.value, equalTo(1)) } - private fun saveForms(instances: List) { - whenever(instancesDataService.instances).doReturn(MutableStateFlow(instances)) + private fun saveForms(projectId: String, instances: List) { + whenever(instancesDataService.getInstances(projectId)).doReturn(MutableStateFlow(instances)) } } From 9c0407fc3a48676cbbeddb9966cdedae88588550 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 11 Jun 2024 14:44:40 +0100 Subject: [PATCH 603/750] Keep different instance lists for different projects --- .../android/instancemanagement/InstancesDataService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 d11d3f34bfd..3c5ace9782e 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 @@ -34,7 +34,7 @@ class InstancesDataService( val sentCount: LiveData = appState.getLive(SENT_COUNT_KEY, 0) fun getInstances(projectId: String): Flow> { - return appState.getFlow("instances", emptyList()) + return appState.getFlow("instances:$projectId", emptyList()) } fun update(projectId: String) { @@ -58,7 +58,7 @@ class InstancesDataService( appState.setLive(EDITABLE_COUNT_KEY, editableInstances) appState.setLive(SENDABLE_COUNT_KEY, sendableInstances) appState.setLive(SENT_COUNT_KEY, sentInstances) - appState.setFlow("instances", instancesRepository.all) + appState.setFlow("instances:$projectId", instancesRepository.all) onUpdate() } From bac2b7189c560a68125ffc4db00920a2d775adc5 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 11 Jun 2024 14:34:11 +0200 Subject: [PATCH 604/750] Created a separate view model for layers data --- .../geo/selection/SelectionMapFragmentTest.kt | 3 +- .../maps/layers/OfflineMapLayersPicker.kt | 72 ++++++++++++++++--- .../layers/OfflineMapLayersPickerAdapter.kt | 23 +++--- .../layers/OfflineMapLayersStateViewModel.kt | 35 +++++++++ .../maps/layers/OfflineMapLayersViewModel.kt | 48 ++----------- 5 files changed, 118 insertions(+), 63 deletions(-) create mode 100644 maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt diff --git a/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt b/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt index 0aec6ff8b13..c941ff75858 100644 --- a/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt @@ -51,6 +51,7 @@ import org.odk.collect.maps.layers.ReferenceLayerRepository import org.odk.collect.material.BottomSheetBehavior import org.odk.collect.material.MaterialProgressDialogFragment import org.odk.collect.permissions.PermissionsChecker +import org.odk.collect.settings.InMemSettingsProvider import org.odk.collect.settings.SettingsProvider import org.odk.collect.testshared.RobolectricHelpers.getFragmentByClass import org.odk.collect.webpage.ExternalWebPageHelper @@ -112,7 +113,7 @@ class SelectionMapFragmentTest { } override fun providesSettingsProvider(): SettingsProvider { - return mock() + return InMemSettingsProvider() } override fun providesExternalWebPageHelper(): ExternalWebPageHelper { diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 9e72e020560..4b5cfa57b94 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -8,17 +8,20 @@ import android.view.ViewGroup import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.odk.collect.androidshared.livedata.LiveDataUtils import org.odk.collect.androidshared.ui.DialogFragmentUtils import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.androidshared.ui.addOnClickListener import org.odk.collect.async.Scheduler import org.odk.collect.maps.databinding.OfflineMapLayersPickerBinding import org.odk.collect.settings.SettingsProvider +import org.odk.collect.settings.keys.ProjectKeys import org.odk.collect.strings.localization.getLocalizedString import org.odk.collect.webpage.ExternalWebPageHelper @@ -30,7 +33,17 @@ class OfflineMapLayersPicker( private val externalWebPageHelper: ExternalWebPageHelper ) : BottomSheetDialogFragment(), OfflineMapLayersPickerAdapter.OfflineMapLayersPickerAdapterInterface { - private val viewModel: OfflineMapLayersViewModel by activityViewModels { + private val stateViewModel: OfflineMapLayersStateViewModel by viewModels { + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return OfflineMapLayersStateViewModel( + settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) + ) as T + } + } + } + + private val sharedViewModel: OfflineMapLayersViewModel by activityViewModels { object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider) as T @@ -42,7 +55,7 @@ class OfflineMapLayersPicker( private val getLayers = registerForActivityResult(ActivityResultContracts.GetMultipleContents(), registry) { uris -> if (uris.isNotEmpty()) { - viewModel.loadLayersToImport(uris, requireContext()) + sharedViewModel.loadLayersToImport(uris, requireContext()) DialogFragmentUtils.showIfNotShowing( OfflineMapLayersImporter::class.java, childFragmentManager @@ -83,7 +96,7 @@ class OfflineMapLayersPicker( } binding.save.setOnClickListener { - viewModel.saveCheckedLayer() + sharedViewModel.saveCheckedLayer(stateViewModel.getCheckedLayer()) dismiss() } @@ -93,7 +106,7 @@ class OfflineMapLayersPicker( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel.isLoading.observe(this) { isLoading -> + sharedViewModel.isLoading.observe(this) { isLoading -> if (isLoading) { binding.progressIndicator.visibility = View.VISIBLE binding.layers.visibility = View.GONE @@ -107,8 +120,12 @@ class OfflineMapLayersPicker( val adapter = OfflineMapLayersPickerAdapter(this) binding.layers.setAdapter(adapter) - viewModel.existingLayers.observe(this) { layers -> - adapter.setData(layers) + LiveDataUtils.zip3( + sharedViewModel.existingLayers, + stateViewModel.checkedLayerId, + stateViewModel.expandedLayerIds + ).observe(this) { (layers, checkedLayerId, expandedLayerIds) -> + updateAdapter(layers, checkedLayerId, expandedLayerIds, adapter) } } @@ -124,11 +141,11 @@ class OfflineMapLayersPicker( } override fun onLayerChecked(layerId: String?) { - viewModel.onLayerChecked(layerId) + stateViewModel.onLayerChecked(layerId) } override fun onLayerToggled(layerId: String?) { - viewModel.onLayerToggled(layerId) + stateViewModel.onLayerToggled(layerId) } override fun onDeleteLayer(layerItem: CheckableReferenceLayer) { @@ -136,13 +153,46 @@ class OfflineMapLayersPicker( .setMessage(requireActivity().getLocalizedString(org.odk.collect.strings.R.string.delete_layer_confirmation_message, layerItem.name)) .setPositiveButton(org.odk.collect.strings.R.string.delete_layer) { _, _ -> layerItem.file?.delete() - if (layerItem.id == viewModel.getCheckedLayer()) { - viewModel.onLayerChecked(null) + if (layerItem.id == stateViewModel.getCheckedLayer()) { + stateViewModel.onLayerChecked(null) } - viewModel.onLayerDeleted(layerItem.id) + stateViewModel.onLayerDeleted(layerItem.id) + sharedViewModel.onLayerDeleted(layerItem.id) } .setNegativeButton(org.odk.collect.strings.R.string.cancel, null) .create() .show() } + + private fun updateAdapter( + layers: List?, + checkedLayerId: String?, + expandedLayerIds: List, + adapter: OfflineMapLayersPickerAdapter + ) { + if (layers == null) { + return + } + + val newData = mutableListOf( + CheckableReferenceLayer( + null, + null, + requireContext().getLocalizedString(org.odk.collect.strings.R.string.none), + checkedLayerId == null, + false + ) + ) + + newData.addAll(layers.map { + CheckableReferenceLayer( + it.id, + it.file, + it.name, + checkedLayerId == it.id, + expandedLayerIds.contains(it.id) + ) + }) + adapter.setData(newData) + } } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt index d0ca54e9749..059c4261bdf 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt @@ -4,12 +4,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.odk.collect.androidshared.ui.addOnClickListener import org.odk.collect.maps.databinding.OfflineMapLayersPickerItemBinding -import org.odk.collect.strings.localization.getLocalizedString +import java.io.File class OfflineMapLayersPickerAdapter( private val listener: OfflineMapLayersPickerAdapterInterface @@ -40,15 +41,9 @@ class OfflineMapLayersPickerAdapter( val layer = asyncListDiffer.currentList[position] holder.binding.radioButton.setChecked(layer.isChecked) - - if (layer.id == null) { - holder.binding.title.text = holder.binding.root.context.getLocalizedString(org.odk.collect.strings.R.string.none) - holder.binding.arrow.visibility = View.GONE - } else { - holder.binding.title.text = layer.name - holder.binding.path.text = layer.file?.absolutePath - holder.binding.arrow.visibility = View.VISIBLE - } + holder.binding.title.text = layer.name + holder.binding.path.text = layer.file?.absolutePath + holder.binding.arrow.isVisible = layer.id != null if (layer.isExpanded) { holder.binding.arrow.setImageDrawable(ContextCompat.getDrawable(holder.binding.root.context, org.odk.collect.icons.R.drawable.ic_baseline_collapse_24)) @@ -81,3 +76,11 @@ class OfflineMapLayersPickerAdapter( class ViewHolder(val binding: OfflineMapLayersPickerItemBinding) : RecyclerView.ViewHolder(binding.root) } + +data class CheckableReferenceLayer( + val id: String?, + val file: File?, + val name: String, + val isChecked: Boolean, + val isExpanded: Boolean +) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt new file mode 100644 index 00000000000..f045dd1b48e --- /dev/null +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt @@ -0,0 +1,35 @@ +package org.odk.collect.maps.layers + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.odk.collect.androidshared.livedata.MutableNonNullLiveData +import org.odk.collect.androidshared.livedata.NonNullLiveData + +class OfflineMapLayersStateViewModel(checkedLayerId: String?) : ViewModel() { + private val _expandedLayerIds = MutableNonNullLiveData>(emptyList()) + val expandedLayerIds: NonNullLiveData> = _expandedLayerIds + + private val _checkedLayerId = MutableLiveData(checkedLayerId) + val checkedLayerId: LiveData = _checkedLayerId + + fun onLayerChecked(layerId: String?) { + _checkedLayerId.postValue(layerId) + } + + fun onLayerToggled(layerId: String?) { + if (_expandedLayerIds.value.contains(layerId)) { + _expandedLayerIds.postValue(_expandedLayerIds.value.filter { it != layerId }) + } else { + _expandedLayerIds.postValue(_expandedLayerIds.value.plus(layerId)) + } + } + + fun onLayerDeleted(layerId: String?) { + _expandedLayerIds.postValue(_expandedLayerIds.value.filter { it != layerId }) + } + + fun getCheckedLayer(): String? { + return checkedLayerId.value + } +} diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index 31bdbeaef0d..0eb7ca62b81 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -22,8 +22,8 @@ class OfflineMapLayersViewModel( private val _isLoading = MutableLiveData() val isLoading: LiveData = _isLoading - private val _existingLayers = MutableLiveData>() - val existingLayers: LiveData> = _existingLayers + private val _existingLayers = MutableLiveData>() + val existingLayers: LiveData> = _existingLayers private val _layersToImport = MutableLiveData>() val layersToImport: LiveData> = _layersToImport @@ -39,13 +39,8 @@ class OfflineMapLayersViewModel( scheduler.immediate( background = { val layers = referenceLayerRepository.getAll() - val checkedLayerId = - settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) - - val newData = mutableListOf(CheckableReferenceLayer(null, null, "", checkedLayerId == null, false)) - newData.addAll(layers.map { CheckableReferenceLayer(it.id, it.file, it.name, it.id == checkedLayerId, false) }) _isLoading.postValue(false) - _existingLayers.postValue(newData) + _existingLayers.postValue(layers) }, foreground = { } ) @@ -92,40 +87,11 @@ class OfflineMapLayersViewModel( ) } - fun saveCheckedLayer() { - settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, getCheckedLayer()) + fun saveCheckedLayer(layerId: String?) { + settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, layerId) } - fun onLayerChecked(layerId: String?) { - _existingLayers.value = _existingLayers.value?.map { - it.copy(isChecked = it.id == layerId) - } - } - - fun onLayerToggled(layerId: String?) { - _existingLayers.value = _existingLayers.value?.map { - val isExpanded = if (it.id == layerId) { - !it.isExpanded - } else { - it.isExpanded - } - it.copy(isExpanded = isExpanded) - } - } - - fun onLayerDeleted(deletedLayerId: String?) { - _existingLayers.value = _existingLayers.value?.filter { it.id != deletedLayerId } - } - - fun getCheckedLayer(): String? { - return _existingLayers.value?.find { it.isChecked }?.id + fun onLayerDeleted(layerId: String?) { + _existingLayers.value = _existingLayers.value?.filter { it.id != layerId } } } - -data class CheckableReferenceLayer( - val id: String?, - val file: File?, - val name: String, - val isChecked: Boolean, - val isExpanded: Boolean -) From 340fa26f81ef6ea0f1e3405f4dc9b445d63818b7 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 12 Jun 2024 15:34:46 +0200 Subject: [PATCH 605/750] Added a failing test --- .../screens/MapsPreferencesFragmentTest.kt | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt diff --git a/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt b/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt new file mode 100644 index 00000000000..a04b3ac3dad --- /dev/null +++ b/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt @@ -0,0 +1,77 @@ +package org.odk.collect.android.preferences.screens + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.gson.Gson +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.odk.collect.android.application.initialization.AnalyticsInitializer +import org.odk.collect.android.application.initialization.MapsInitializer +import org.odk.collect.android.injection.config.AppDependencyModule +import org.odk.collect.android.projects.ProjectsDataService +import org.odk.collect.android.support.CollectHelpers +import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule +import org.odk.collect.projects.Project +import org.odk.collect.projects.ProjectsRepository +import org.odk.collect.settings.InMemSettingsProvider +import org.odk.collect.settings.SettingsProvider +import org.odk.collect.shared.strings.UUIDGenerator + +@RunWith(AndroidJUnit4::class) +class MapsPreferencesFragmentTest { + + @get:Rule + val launcherRule = FragmentScenarioLauncherRule() + + private val project = Project.DEMO_PROJECT + private val projectsDataService = mock().apply { + whenever(getCurrentProject()).thenReturn(project) + } + private val projectsRepository = mock().apply { + whenever(get(project.uuid)).thenReturn(project) + } + private val settingsProvider = InMemSettingsProvider() + + @Before + fun setup() { + CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() { + override fun providesCurrentProjectProvider( + settingsProvider: SettingsProvider, + projectsRepository: ProjectsRepository, + analyticsInitializer: AnalyticsInitializer, + context: Context, + mapsInitializer: MapsInitializer + ): ProjectsDataService { + return projectsDataService + } + + override fun providesProjectsRepository( + uuidGenerator: UUIDGenerator, + gson: Gson, + settingsProvider: SettingsProvider + ): ProjectsRepository { + return projectsRepository + } + + override fun providesSettingsProvider(context: Context): SettingsProvider { + return settingsProvider + } + }) + } + + @Test + fun `if saved layer does not exist it is set to 'none'`() { + val settings = settingsProvider.getUnprotectedSettings() + settings.save(MapsPreferencesFragment.REFERENCE_LAYER_KEY, "blah") + + launcherRule.launch(MapsPreferencesFragment::class.java) + + assertThat(settings.getString(MapsPreferencesFragment.REFERENCE_LAYER_KEY), equalTo(null)) + } +} From 5a49b4480ea2249cf9baac994de6ef4110e003b5 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 12 Jun 2024 15:35:11 +0200 Subject: [PATCH 606/750] Handle non-existing layers --- .../preferences/screens/MapsPreferencesFragment.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt index b6948878a19..0f90ad0766c 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt @@ -147,9 +147,14 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP baseCategory.addPreference(pref) } - // Clear the reference layer if it isn't supported by the new basemap. + // Clear the reference layer if it does not exist or it isn't supported by the new basemap. val layerId = settingsProvider.getUnprotectedSettings().getString(REFERENCE_LAYER_KEY) - if (layerId != null && !cftor.supportsLayer(referenceLayerRepository.get(layerId)!!.file)) { + if (layerId != null) { + val layer = referenceLayerRepository.get(layerId) + if (layer == null || !cftor.supportsLayer(layer.file)) { + settingsProvider.getUnprotectedSettings().save(REFERENCE_LAYER_KEY, null) + } + settingsProvider.getUnprotectedSettings().save(REFERENCE_LAYER_KEY, null) } } From 4e9122104e45b7df8ab9a5dab435979485754188 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 12 Jun 2024 15:40:28 +0200 Subject: [PATCH 607/750] Use one const to refernce to saved offline layers --- .../screens/MapsPreferencesFragment.kt | 21 ++++++++----------- .../screens/MapsPreferencesFragmentTest.kt | 5 +++-- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt index 0f90ad0766c..61ca8b776dd 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt @@ -29,6 +29,7 @@ import org.odk.collect.async.Scheduler import org.odk.collect.maps.MapConfigurator import org.odk.collect.maps.layers.OfflineMapLayersPicker import org.odk.collect.maps.layers.ReferenceLayerRepository +import org.odk.collect.settings.keys.ProjectKeys import org.odk.collect.settings.keys.ProjectKeys.CATEGORY_BASEMAP import org.odk.collect.settings.keys.ProjectKeys.KEY_BASEMAP_SOURCE import org.odk.collect.strings.localization.getLocalizedString @@ -65,8 +66,8 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP override fun onSettingChanged(key: String) { super.onSettingChanged(key) - if (key == REFERENCE_LAYER_KEY) { - findPreference(REFERENCE_LAYER_KEY)!!.summary = getLayerName() + if (key == ProjectKeys.KEY_REFERENCE_LAYER) { + findPreference(ProjectKeys.KEY_REFERENCE_LAYER)!!.summary = getLayerName() } } @@ -80,7 +81,7 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP override fun onPreferenceClick(preference: Preference): Boolean { if (allowClick(javaClass.name)) { when (preference.key) { - REFERENCE_LAYER_KEY -> { + ProjectKeys.KEY_REFERENCE_LAYER -> { DialogFragmentUtils.showIfNotShowing( OfflineMapLayersPicker::class.java, childFragmentManager @@ -121,14 +122,14 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP } private fun initLayersPref() { - findPreference(REFERENCE_LAYER_KEY)?.apply { + findPreference(ProjectKeys.KEY_REFERENCE_LAYER)?.apply { onPreferenceClickListener = this@MapsPreferencesFragment summary = getLayerName() } } private fun getLayerName(): String { - val layerId = settingsProvider.getUnprotectedSettings().getString(REFERENCE_LAYER_KEY) + val layerId = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) return if (layerId == null) { requireContext().getLocalizedString(org.odk.collect.strings.R.string.none) } else { @@ -148,18 +149,14 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP } // Clear the reference layer if it does not exist or it isn't supported by the new basemap. - val layerId = settingsProvider.getUnprotectedSettings().getString(REFERENCE_LAYER_KEY) + val layerId = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) if (layerId != null) { val layer = referenceLayerRepository.get(layerId) if (layer == null || !cftor.supportsLayer(layer.file)) { - settingsProvider.getUnprotectedSettings().save(REFERENCE_LAYER_KEY, null) + settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, null) } - settingsProvider.getUnprotectedSettings().save(REFERENCE_LAYER_KEY, null) + settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, null) } } - - companion object { - const val REFERENCE_LAYER_KEY = "reference_layer" - } } diff --git a/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt b/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt index a04b3ac3dad..778f16d725f 100644 --- a/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt @@ -21,6 +21,7 @@ import org.odk.collect.projects.Project import org.odk.collect.projects.ProjectsRepository import org.odk.collect.settings.InMemSettingsProvider import org.odk.collect.settings.SettingsProvider +import org.odk.collect.settings.keys.ProjectKeys import org.odk.collect.shared.strings.UUIDGenerator @RunWith(AndroidJUnit4::class) @@ -68,10 +69,10 @@ class MapsPreferencesFragmentTest { @Test fun `if saved layer does not exist it is set to 'none'`() { val settings = settingsProvider.getUnprotectedSettings() - settings.save(MapsPreferencesFragment.REFERENCE_LAYER_KEY, "blah") + settings.save(ProjectKeys.KEY_REFERENCE_LAYER, "blah") launcherRule.launch(MapsPreferencesFragment::class.java) - assertThat(settings.getString(MapsPreferencesFragment.REFERENCE_LAYER_KEY), equalTo(null)) + assertThat(settings.getString(ProjectKeys.KEY_REFERENCE_LAYER), equalTo(null)) } } From 4560eaf30dad25b52c29e8b0fc338908945b5bef Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 12 Jun 2024 16:17:30 +0200 Subject: [PATCH 608/750] Removed redundant attribute --- maps/src/main/res/layout/offline_map_layers_picker_item.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/maps/src/main/res/layout/offline_map_layers_picker_item.xml b/maps/src/main/res/layout/offline_map_layers_picker_item.xml index 37a1a8ce499..60c63973061 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker_item.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker_item.xml @@ -45,7 +45,6 @@ android:layout_marginBottom="@dimen/margin_extra_small" android:layout_marginTop="@dimen/margin_extra_small" app:icon="@drawable/ic_add_white_24" - app:iconTint="?colorAccent" app:layout_constraintStart_toStartOf="@id/title" app:layout_constraintTop_toBottomOf="@id/path" /> From b121b70b1fbbc657db734c2ae58de50992a426ad Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 12 Jun 2024 15:17:43 +0100 Subject: [PATCH 609/750] Maintain ranking item color when dragging --- .../odk/collect/android/adapters/RankingListAdapter.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/adapters/RankingListAdapter.java b/collect_app/src/main/java/org/odk/collect/android/adapters/RankingListAdapter.java index b8ee10ab002..6ed6685844c 100644 --- a/collect_app/src/main/java/org/odk/collect/android/adapters/RankingListAdapter.java +++ b/collect_app/src/main/java/org/odk/collect/android/adapters/RankingListAdapter.java @@ -16,6 +16,7 @@ package org.odk.collect.android.adapters; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.GradientDrawable; import android.view.LayoutInflater; import android.view.View; @@ -77,23 +78,25 @@ public static class ItemViewHolder extends ViewHolder { final TextView textView; final ThemeUtils themeUtils; + private final ColorDrawable background; ItemViewHolder(View itemView) { super(itemView); textView = itemView.findViewById(R.id.rank_item_text); textView.setTextSize(QuestionFontSizeUtils.getQuestionFontSize()); themeUtils = new ThemeUtils(itemView.getContext()); + background = (ColorDrawable) itemView.getBackground(); } public void onItemSelected() { GradientDrawable border = new GradientDrawable(); border.setStroke(10, themeUtils.getAccentColor()); - border.setColor(textView.getContext().getResources().getColor(R.color.colorSurfaceContainerLow)); + border.setColor(background.getColor()); itemView.setBackground(border); } public void onItemClear() { - itemView.setBackgroundColor(textView.getContext().getResources().getColor(R.color.colorSurfaceContainerLow)); + itemView.setBackground(background); } } } From 03aa8ee558f5e3d8444c7019f57950b8d9781b30 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 12 Jun 2024 16:20:33 +0200 Subject: [PATCH 610/750] Moved reading the saved layer id from UI to viewmodel --- .../org/odk/collect/maps/layers/OfflineMapLayersPicker.kt | 5 +---- .../collect/maps/layers/OfflineMapLayersStateViewModel.kt | 7 +++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 4b5cfa57b94..f83cfd711a2 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -21,7 +21,6 @@ import org.odk.collect.androidshared.ui.addOnClickListener import org.odk.collect.async.Scheduler import org.odk.collect.maps.databinding.OfflineMapLayersPickerBinding import org.odk.collect.settings.SettingsProvider -import org.odk.collect.settings.keys.ProjectKeys import org.odk.collect.strings.localization.getLocalizedString import org.odk.collect.webpage.ExternalWebPageHelper @@ -36,9 +35,7 @@ class OfflineMapLayersPicker( private val stateViewModel: OfflineMapLayersStateViewModel by viewModels { object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return OfflineMapLayersStateViewModel( - settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) - ) as T + return OfflineMapLayersStateViewModel(settingsProvider) as T } } } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt index f045dd1b48e..b4e14c10afa 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt @@ -5,12 +5,15 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.odk.collect.androidshared.livedata.MutableNonNullLiveData import org.odk.collect.androidshared.livedata.NonNullLiveData +import org.odk.collect.settings.SettingsProvider +import org.odk.collect.settings.keys.ProjectKeys -class OfflineMapLayersStateViewModel(checkedLayerId: String?) : ViewModel() { +class OfflineMapLayersStateViewModel(settingsProvider: SettingsProvider) : ViewModel() { private val _expandedLayerIds = MutableNonNullLiveData>(emptyList()) val expandedLayerIds: NonNullLiveData> = _expandedLayerIds - private val _checkedLayerId = MutableLiveData(checkedLayerId) + private val _checkedLayerId = + MutableLiveData(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER)) val checkedLayerId: LiveData = _checkedLayerId fun onLayerChecked(layerId: String?) { From 0248104f231958db39dc47247be856536bba4bb2 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 12 Jun 2024 16:58:44 +0200 Subject: [PATCH 611/750] Use setValue instead of postValue --- .../collect/maps/layers/OfflineMapLayersStateViewModel.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt index b4e14c10afa..ffe218b7240 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt @@ -17,19 +17,19 @@ class OfflineMapLayersStateViewModel(settingsProvider: SettingsProvider) : ViewM val checkedLayerId: LiveData = _checkedLayerId fun onLayerChecked(layerId: String?) { - _checkedLayerId.postValue(layerId) + _checkedLayerId.value = layerId } fun onLayerToggled(layerId: String?) { if (_expandedLayerIds.value.contains(layerId)) { - _expandedLayerIds.postValue(_expandedLayerIds.value.filter { it != layerId }) + _expandedLayerIds.value = _expandedLayerIds.value.filter { it != layerId } } else { - _expandedLayerIds.postValue(_expandedLayerIds.value.plus(layerId)) + _expandedLayerIds.value = _expandedLayerIds.value.plus(layerId) } } fun onLayerDeleted(layerId: String?) { - _expandedLayerIds.postValue(_expandedLayerIds.value.filter { it != layerId }) + _expandedLayerIds.value = _expandedLayerIds.value.filter { it != layerId } } fun getCheckedLayer(): String? { From 89932b61fac08d1ab7461cb85ea93459cf9e9473 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 12 Jun 2024 17:27:07 +0200 Subject: [PATCH 612/750] Moved deleting layer filas to viewmodel --- .../maps/layers/OfflineMapLayersPicker.kt | 3 +-- .../maps/layers/OfflineMapLayersViewModel.kt | 9 +++++-- .../maps/layers/OfflineMapLayersPickerTest.kt | 24 +++++++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index f83cfd711a2..f5eaafbe69c 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -149,12 +149,11 @@ class OfflineMapLayersPicker( MaterialAlertDialogBuilder(requireActivity()) .setMessage(requireActivity().getLocalizedString(org.odk.collect.strings.R.string.delete_layer_confirmation_message, layerItem.name)) .setPositiveButton(org.odk.collect.strings.R.string.delete_layer) { _, _ -> - layerItem.file?.delete() if (layerItem.id == stateViewModel.getCheckedLayer()) { stateViewModel.onLayerChecked(null) } stateViewModel.onLayerDeleted(layerItem.id) - sharedViewModel.onLayerDeleted(layerItem.id) + sharedViewModel.onLayerDeleted(layerItem.id, layerItem.file) } .setNegativeButton(org.odk.collect.strings.R.string.cancel, null) .create() diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index 0eb7ca62b81..041dcb585e6 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -91,7 +91,12 @@ class OfflineMapLayersViewModel( settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, layerId) } - fun onLayerDeleted(layerId: String?) { - _existingLayers.value = _existingLayers.value?.filter { it.id != layerId } + fun onLayerDeleted(layerId: String?, layerFile: File?) { + _isLoading.value = true + scheduler.immediate { + layerFile?.delete() + _existingLayers.postValue(_existingLayers.value?.filter { it.id != layerId }) + _isLoading.postValue(false) + } } } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index 3ae8b7c3be6..52f59dfebfd 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -499,6 +499,30 @@ class OfflineMapLayersPickerTest { assertThat(layerFile2.exists(), equalTo(true)) } + @Test + fun `progress indicator is displayed during deleting layers`() { + val layerFile1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + ReferenceLayer("1", layerFile1, "layer1"), + )) + + launchFragment() + + scheduler.flush() + + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.arrow)).perform(click()) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform(scrollTo(), click()) + onView(withText(string.delete_layer)).inRoot(isDialog()).perform(click()) + + onView(withId(R.id.progress_indicator)).check(matches(isDisplayed())) + onView(withId(R.id.layers)).check(matches(not(isDisplayed()))) + + scheduler.flush() + + onView(withId(R.id.progress_indicator)).check(matches(not(isDisplayed()))) + onView(withId(R.id.layers)).check(matches(isDisplayed())) + } + private fun assertLayerCollapsed(position: Int) { onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.arrow)).check(matches(withImageDrawable(org.odk.collect.icons.R.drawable.ic_baseline_expand_24))) onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.path)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) From 73a642ee6b1bf350f003450a376e5814966f6ba3 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 12 Jun 2024 17:43:37 +0200 Subject: [PATCH 613/750] Added #delete method to ReferenceLayerRepository --- .../DirectoryReferenceLayerRepository.kt | 4 ++++ .../maps/layers/OfflineMapLayersPicker.kt | 2 +- .../maps/layers/OfflineMapLayersViewModel.kt | 4 ++-- .../maps/layers/ReferenceLayerRepository.kt | 1 + .../DirectoryReferenceLayerRepositoryTest.kt | 18 ++++++++++++++++++ .../maps/layers/OfflineMapLayersPickerTest.kt | 7 ++++--- 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt index 384385ffb35..8e5d2aaca14 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt @@ -36,6 +36,10 @@ class DirectoryReferenceLayerRepository( } } + override fun delete(id: String) { + get(id)?.file?.delete() + } + private fun getAllFilesWithDirectory() = listOf(sharedLayersDirPath, projectLayersDirPath).flatMap { dir -> listFilesRecursively(File(dir)).map { file -> Pair(file, dir) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index f5eaafbe69c..32577f51e7d 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -153,7 +153,7 @@ class OfflineMapLayersPicker( stateViewModel.onLayerChecked(null) } stateViewModel.onLayerDeleted(layerItem.id) - sharedViewModel.onLayerDeleted(layerItem.id, layerItem.file) + sharedViewModel.onLayerDeleted(layerItem.id!!) } .setNegativeButton(org.odk.collect.strings.R.string.cancel, null) .create() diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index 041dcb585e6..989db59b5ec 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -91,10 +91,10 @@ class OfflineMapLayersViewModel( settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, layerId) } - fun onLayerDeleted(layerId: String?, layerFile: File?) { + fun onLayerDeleted(layerId: String) { _isLoading.value = true scheduler.immediate { - layerFile?.delete() + referenceLayerRepository.delete(layerId) _existingLayers.postValue(_existingLayers.value?.filter { it.id != layerId }) _isLoading.postValue(false) } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt index f8dadbaa540..a7520708d7e 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt @@ -7,6 +7,7 @@ interface ReferenceLayerRepository { fun getAll(): List fun get(id: String): ReferenceLayer? fun addLayer(file: File, shared: Boolean) + fun delete(id: String) } data class ReferenceLayer(val id: String, val file: File, val name: String) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index e8e52ce54f2..36208b2b708 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Bundle import androidx.preference.Preference import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.containsInAnyOrder import org.hamcrest.Matchers.equalTo import org.junit.Test @@ -176,6 +177,23 @@ class DirectoryReferenceLayerRepositoryTest { assertThat(projectLayersDir.listFiles()[0].readText(), equalTo("blah")) } + @Test + fun delete_returnsLayerWithCorrectName() { + val file1 = TempFiles.createTempFile(sharedLayersDir) + val file2 = TempFiles.createTempFile(sharedLayersDir) + + mapConfigurator.apply { + addFile(file1, true, file2.name) + addFile(file2, true, file2.name) + } + + val fileLayer1 = repository.getAll().first { it.file == file1 } + val fileLayer2 = repository.getAll().first { it.file == file2 } + repository.delete(fileLayer1.id) + + assertThat(repository.getAll(), contains(fileLayer2)) + } + private class StubMapConfigurator : MapConfigurator { private val files = mutableMapOf>() diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index 52f59dfebfd..a95cc7d0053 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -32,6 +32,7 @@ import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.odk.collect.androidshared.ui.FragmentFactoryBuilder @@ -470,7 +471,7 @@ class OfflineMapLayersPickerTest { onView(withId(R.id.layers)).perform(scrollToPosition(0)) onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.title)).check(matches(withText("layer1"))) - assertThat(layerFile1.exists(), equalTo(true)) + verify(referenceLayerRepository, never()).delete("1") } @Test @@ -495,8 +496,8 @@ class OfflineMapLayersPickerTest { onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.title)).check(matches(withText("layer2"))) - assertThat(layerFile1.exists(), equalTo(false)) - assertThat(layerFile2.exists(), equalTo(true)) + verify(referenceLayerRepository).delete("1") + verify(referenceLayerRepository, never()).delete("2") } @Test From a952ce9da225d50326b2fb6e4b2d6f6104301c9c Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 12 Jun 2024 17:26:58 +0100 Subject: [PATCH 614/750] Prevent double taps on multi select actions --- lists/src/main/res/layout/multi_select_controls_layout.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lists/src/main/res/layout/multi_select_controls_layout.xml b/lists/src/main/res/layout/multi_select_controls_layout.xml index db1cb3bc672..868ed9e5d4c 100644 --- a/lists/src/main/res/layout/multi_select_controls_layout.xml +++ b/lists/src/main/res/layout/multi_select_controls_layout.xml @@ -19,7 +19,7 @@ android:layout_weight="1" android:text="@string/select_all" /> - Date: Wed, 12 Jun 2024 23:15:30 +0200 Subject: [PATCH 615/750] Do not support the masked appearance in numeric questions --- .../formhierarchy/QuestionAnswerProcessor.kt | 7 +- .../collect/android/utilities/Appearances.kt | 3 +- .../QuestionAnswerProcessorTest.kt | 88 ++++++++----------- .../android/utilities/AppearancesTest.kt | 19 +++- .../android/widgets/DecimalWidgetTest.java | 17 ++-- .../android/widgets/ExDecimalWidgetTest.java | 18 ++-- .../android/widgets/ExIntegerWidgetTest.java | 19 ++-- .../android/widgets/ExStringWidgetTest.java | 21 ++++- .../android/widgets/IntegerWidgetTest.java | 18 ++-- .../widgets/StringNumberWidgetTest.java | 21 ++++- .../android/widgets/StringWidgetTest.java | 21 ++++- .../base/GeneralExStringWidgetTest.java | 21 ----- .../widgets/base/GeneralStringWidgetTest.java | 23 +---- 13 files changed, 160 insertions(+), 136 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessor.kt b/collect_app/src/main/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessor.kt index fbc705f9e2c..3604bda99a7 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessor.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessor.kt @@ -27,11 +27,8 @@ object QuestionAnswerProcessor { if (!fep.answerText.isNullOrBlank() && Appearances.isMasked(fep) && - fep.controlType == Constants.CONTROL_INPUT && ( - fep.dataType == Constants.DATATYPE_TEXT || - fep.dataType == Constants.DATATYPE_INTEGER || - fep.dataType == Constants.DATATYPE_DECIMAL - ) + fep.controlType == Constants.CONTROL_INPUT && + fep.dataType == Constants.DATATYPE_TEXT ) { return "••••••••••" } diff --git a/collect_app/src/main/java/org/odk/collect/android/utilities/Appearances.kt b/collect_app/src/main/java/org/odk/collect/android/utilities/Appearances.kt index 5ae33fb4666..78d4067709f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/utilities/Appearances.kt +++ b/collect_app/src/main/java/org/odk/collect/android/utilities/Appearances.kt @@ -16,6 +16,7 @@ package org.odk.collect.android.utilities import android.content.res.Configuration +import org.javarosa.core.model.Constants import org.javarosa.form.api.FormEntryPrompt import org.odk.collect.android.dynamicpreload.ExternalDataUtil import org.odk.collect.androidshared.utils.ScreenUtils @@ -195,6 +196,6 @@ object Appearances { @JvmStatic fun isMasked(prompt: FormEntryPrompt): Boolean { val appearance = getSanitizedAppearanceHint(prompt) - return appearance.contains(MASKED) + return appearance.contains(MASKED) && prompt.dataType == Constants.DATATYPE_TEXT } } diff --git a/collect_app/src/test/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessorTest.kt b/collect_app/src/test/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessorTest.kt index b7774d5a114..d615b0f6023 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessorTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessorTest.kt @@ -27,73 +27,57 @@ class QuestionAnswerProcessorTest { } @Test - fun noAnswerShouldBeDisplayedIfItDoesNotExistAndMaskedAppearanceIsUsedForTextAndNumberDataTypes() { - listOf( - Constants.DATATYPE_TEXT, - Constants.DATATYPE_INTEGER, - Constants.DATATYPE_DECIMAL - ).forEach { - val question = mock() - val prompt = MockFormEntryPromptBuilder() - .withQuestion(question) - .withDataType(it) - .withControlType(Constants.CONTROL_INPUT) - .withAppearance(Appearances.MASKED) - .build() + fun noAnswerShouldBeDisplayedIfItDoesNotExistAndMaskedAppearanceIsUsedForTextDataTypes() { + val question = mock() + val prompt = MockFormEntryPromptBuilder() + .withQuestion(question) + .withDataType(Constants.DATATYPE_TEXT) + .withControlType(Constants.CONTROL_INPUT) + .withAppearance(Appearances.MASKED) + .build() - val answer = QuestionAnswerProcessor.getQuestionAnswer(prompt, mock(), mock()) + val answer = QuestionAnswerProcessor.getQuestionAnswer(prompt, mock(), mock()) - assertThat(answer, equalTo("")) - } + assertThat(answer, equalTo("")) } @Test - fun noAnswerShouldBeDisplayedIfItIsEmptyAndMaskedAppearanceIsUsedForTextAndNumberDataTypes() { - listOf( - Constants.DATATYPE_TEXT, - Constants.DATATYPE_INTEGER, - Constants.DATATYPE_DECIMAL - ).forEach { - val question = mock() - val prompt = MockFormEntryPromptBuilder() - .withQuestion(question) - .withAnswerDisplayText("") - .withAppearance(Appearances.MASKED) - .withControlType(Constants.CONTROL_INPUT) - .withDataType(it) - .build() + fun noAnswerShouldBeDisplayedIfItIsEmptyAndMaskedAppearanceIsUsedForTextDataTypes() { + val question = mock() + val prompt = MockFormEntryPromptBuilder() + .withQuestion(question) + .withAnswerDisplayText("") + .withAppearance(Appearances.MASKED) + .withControlType(Constants.CONTROL_INPUT) + .withDataType(Constants.DATATYPE_TEXT) + .build() - val answer = QuestionAnswerProcessor.getQuestionAnswer(prompt, mock(), mock()) + val answer = QuestionAnswerProcessor.getQuestionAnswer(prompt, mock(), mock()) - assertThat(answer, equalTo("")) - } + assertThat(answer, equalTo("")) } @Test - fun maskedAnswerShouldBeDisplayedIfItExistAndMaskedAppearanceIsUsedForTextAndNumberDataTypes() { - listOf( - Constants.DATATYPE_TEXT, - Constants.DATATYPE_INTEGER, - Constants.DATATYPE_DECIMAL - ).forEach { - val question = mock() - val prompt = MockFormEntryPromptBuilder() - .withQuestion(question) - .withAnswerDisplayText("blah") - .withAppearance(Appearances.MASKED) - .withControlType(Constants.CONTROL_INPUT) - .withDataType(it) - .build() + fun maskedAnswerShouldBeDisplayedIfItExistAndMaskedAppearanceIsUsedForTextDataTypes() { + val question = mock() + val prompt = MockFormEntryPromptBuilder() + .withQuestion(question) + .withAnswerDisplayText("blah") + .withAppearance(Appearances.MASKED) + .withControlType(Constants.CONTROL_INPUT) + .withDataType(Constants.DATATYPE_TEXT) + .build() - val answer = QuestionAnswerProcessor.getQuestionAnswer(prompt, mock(), mock()) + val answer = QuestionAnswerProcessor.getQuestionAnswer(prompt, mock(), mock()) - assertThat(answer, equalTo("••••••••••")) - } + assertThat(answer, equalTo("••••••••••")) } @Test - fun originalAnswerShouldBeDisplayedIfItExistAndMaskedAppearanceIsUsedForDataTypesOtherThanTextAndNumber() { + fun originalAnswerShouldBeDisplayedIfItExistAndMaskedAppearanceIsUsedForDataTypesOtherThanText() { listOf( + Constants.DATATYPE_INTEGER, + Constants.DATATYPE_DECIMAL, Constants.DATATYPE_DATE_TIME, Constants.DATATYPE_DATE, Constants.DATATYPE_TIME, @@ -120,7 +104,7 @@ class QuestionAnswerProcessorTest { } @Test - fun originalAnswerShouldBeDisplayedIfItExistAndMaskedAppearanceIsUsedButControlTypeIsOtherThanInput() { + fun originalAnswerShouldBeDisplayedIfItExistAndMaskedAppearanceIsUsedForDataTypesAndControlTypeOtherThanInput() { listOf( Constants.CONTROL_RANGE, Constants.CONTROL_RANK, diff --git a/collect_app/src/test/java/org/odk/collect/android/utilities/AppearancesTest.kt b/collect_app/src/test/java/org/odk/collect/android/utilities/AppearancesTest.kt index e389e0090fb..aa718aa2363 100644 --- a/collect_app/src/test/java/org/odk/collect/android/utilities/AppearancesTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/utilities/AppearancesTest.kt @@ -19,6 +19,7 @@ import android.content.res.Configuration import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue +import org.javarosa.core.model.Constants import org.javarosa.form.api.FormEntryPrompt import org.junit.Test import org.mockito.kotlin.mock @@ -233,7 +234,8 @@ class AppearancesTest { } @Test - fun `useThousandSeparator returns false when 'thousands-sep' appearance is found but mixed with 'masked'`() { + fun `useThousandSeparator returns false when 'thousands-sep' appearance is found but mixed with 'masked' for text questions`() { + whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_TEXT) whenever(formEntryPrompt.appearanceHint).thenReturn("thousands-sep masked") assertFalse(Appearances.useThousandSeparator(formEntryPrompt)) } @@ -369,6 +371,7 @@ class AppearancesTest { @Test fun `isMasked returns false when there is no appearance`() { + whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_TEXT) assertFalse(Appearances.isMasked(formEntryPrompt)) whenever(formEntryPrompt.appearanceHint).thenReturn("") @@ -377,13 +380,25 @@ class AppearancesTest { @Test fun `isMasked returns false when non of supported appearances is found`() { + whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_TEXT) whenever(formEntryPrompt.appearanceHint).thenReturn("blah") assertFalse(Appearances.isMasked(formEntryPrompt)) } @Test - fun `isMasked returns true when 'masked' appearance is found`() { + fun `isMasked returns true when 'masked' appearance is found for text questions`() { + whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_TEXT) whenever(formEntryPrompt.appearanceHint).thenReturn("masked") assertTrue(Appearances.isMasked(formEntryPrompt)) } + + @Test + fun `isMasked returns false when 'masked' appearance is found for numeric questions`() { + whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_INTEGER) + whenever(formEntryPrompt.appearanceHint).thenReturn("masked") + assertFalse(Appearances.isMasked(formEntryPrompt)) + + whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_DECIMAL) + assertFalse(Appearances.isMasked(formEntryPrompt)) + } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/DecimalWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/DecimalWidgetTest.java index cd1293f7596..baeed76c95a 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/DecimalWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/DecimalWidgetTest.java @@ -2,12 +2,14 @@ import androidx.annotation.NonNull; +import org.javarosa.core.model.Constants; import org.javarosa.core.model.QuestionDef; import org.javarosa.core.model.data.DecimalData; import org.javarosa.core.model.data.IAnswerData; import org.junit.Test; import org.mockito.Mock; import org.odk.collect.android.formentry.questions.QuestionDetails; +import org.odk.collect.android.utilities.Appearances; import org.odk.collect.android.widgets.base.GeneralStringWidgetTest; import java.text.NumberFormat; @@ -15,12 +17,13 @@ import java.util.Random; import static junit.framework.TestCase.assertEquals; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.when; -import static org.odk.collect.android.utilities.Appearances.MASKED; import static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP; import android.text.InputType; @@ -40,6 +43,7 @@ public class DecimalWidgetTest extends GeneralStringWidgetTest Date: Thu, 13 Jun 2024 11:48:58 +0200 Subject: [PATCH 616/750] Do not remove layerId from the list of expanded ids after removing the layer --- .../org/odk/collect/maps/layers/OfflineMapLayersPicker.kt | 1 - .../odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt | 4 ---- 2 files changed, 5 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 32577f51e7d..982679f0d3b 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -152,7 +152,6 @@ class OfflineMapLayersPicker( if (layerItem.id == stateViewModel.getCheckedLayer()) { stateViewModel.onLayerChecked(null) } - stateViewModel.onLayerDeleted(layerItem.id) sharedViewModel.onLayerDeleted(layerItem.id!!) } .setNegativeButton(org.odk.collect.strings.R.string.cancel, null) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt index ffe218b7240..b54dce05d3a 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt @@ -28,10 +28,6 @@ class OfflineMapLayersStateViewModel(settingsProvider: SettingsProvider) : ViewM } } - fun onLayerDeleted(layerId: String?) { - _expandedLayerIds.value = _expandedLayerIds.value.filter { it != layerId } - } - fun getCheckedLayer(): String? { return checkedLayerId.value } From 0e7ec68e2e3545c08bae396253fa91dc650babbc Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 11:49:38 +0200 Subject: [PATCH 617/750] Fixed method name --- .../java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt | 2 +- .../org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 982679f0d3b..93371897be7 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -152,7 +152,7 @@ class OfflineMapLayersPicker( if (layerItem.id == stateViewModel.getCheckedLayer()) { stateViewModel.onLayerChecked(null) } - sharedViewModel.onLayerDeleted(layerItem.id!!) + sharedViewModel.deleteLayer(layerItem.id!!) } .setNegativeButton(org.odk.collect.strings.R.string.cancel, null) .create() diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index 989db59b5ec..893104dc060 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -91,7 +91,7 @@ class OfflineMapLayersViewModel( settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, layerId) } - fun onLayerDeleted(layerId: String) { + fun deleteLayer(layerId: String) { _isLoading.value = true scheduler.immediate { referenceLayerRepository.delete(layerId) From 624e8c990a2c729fbb9b4bd14089d912549054b5 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 11:50:05 +0200 Subject: [PATCH 618/750] Fixed test name --- .../maps/layers/DirectoryReferenceLayerRepositoryTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index 36208b2b708..9529661bc05 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -178,7 +178,7 @@ class DirectoryReferenceLayerRepositoryTest { } @Test - fun delete_returnsLayerWithCorrectName() { + fun delete_deletesLayerWithId() { val file1 = TempFiles.createTempFile(sharedLayersDir) val file2 = TempFiles.createTempFile(sharedLayersDir) From 81534ffa93604ea160a650ad8280c27014b81257 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 16:26:06 +0200 Subject: [PATCH 619/750] Fixed CollectGeoDependencyModule --- .../collect/android/application/Collect.java | 10 +-------- .../config/CollectGeoDependencyModule.kt | 22 +++++++------------ 2 files changed, 9 insertions(+), 23 deletions(-) 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 b4ca27264d7..77b7d94be8e 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 @@ -300,15 +300,7 @@ public GeoDependencyComponent getGeoDependencyComponent() { if (geoDependencyComponent == null) { geoDependencyComponent = DaggerGeoDependencyComponent.builder() .application(this) - .geoDependencyModule(new CollectGeoDependencyModule( - applicationComponent.mapFragmentFactory(), - applicationComponent.locationClient(), - applicationComponent.scheduler(), - applicationComponent.permissionsChecker(), - applicationComponent.referenceLayerRepository(), - applicationComponent.settingsProvider(), - applicationComponent.externalWebPageHelper() - )) + .geoDependencyModule(new CollectGeoDependencyModule(applicationComponent)) .build(); } diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGeoDependencyModule.kt b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGeoDependencyModule.kt index 58c0883eb07..f5373dc0344 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGeoDependencyModule.kt +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGeoDependencyModule.kt @@ -17,17 +17,11 @@ import org.odk.collect.settings.SettingsProvider import org.odk.collect.webpage.ExternalWebPageHelper class CollectGeoDependencyModule( - private val mapFragmentFactory: MapFragmentFactory, - private val locationClient: LocationClient, - private val scheduler: Scheduler, - private val permissionChecker: PermissionsChecker, - private val referenceLayerRepository: ReferenceLayerRepository, - private val settingsProvider: SettingsProvider, - private val externalWebPageHelper: ExternalWebPageHelper + private val appDependencyComponent: AppDependencyComponent ) : GeoDependencyModule() { override fun providesMapFragmentFactory(): MapFragmentFactory { - return mapFragmentFactory + return appDependencyComponent.mapFragmentFactory() } override fun providesLocationTracker(application: Application): LocationTracker { @@ -35,11 +29,11 @@ class CollectGeoDependencyModule( } override fun providesLocationClient(): LocationClient { - return locationClient + return appDependencyComponent.locationClient() } override fun providesScheduler(): Scheduler { - return scheduler + return appDependencyComponent.scheduler() } override fun providesSatelliteInfoClient(context: Context): SatelliteInfoClient { @@ -49,18 +43,18 @@ class CollectGeoDependencyModule( } override fun providesPermissionChecker(context: Context): PermissionsChecker { - return permissionChecker + return appDependencyComponent.permissionsChecker() } override fun providesReferenceLayerRepository(): ReferenceLayerRepository { - return referenceLayerRepository + return appDependencyComponent.referenceLayerRepository() } override fun providesSettingsProvider(): SettingsProvider { - return settingsProvider + return appDependencyComponent.settingsProvider() } override fun providesExternalWebPageHelper(): ExternalWebPageHelper { - return externalWebPageHelper + return appDependencyComponent.externalWebPageHelper() } } From 756e80210fb1ccda90f42804afb741652a0a309d Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 16:26:48 +0200 Subject: [PATCH 620/750] Fixed CollectGoogleMapsDependencyModule --- .../org/odk/collect/android/application/Collect.java | 6 +----- .../config/CollectGoogleMapsDependencyModule.kt | 10 ++++------ 2 files changed, 5 insertions(+), 11 deletions(-) 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 77b7d94be8e..a7c0d2babc4 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 @@ -365,11 +365,7 @@ public SelfieCameraDependencyComponent getSelfieCameraDependencyComponent() { public GoogleMapsDependencyComponent getGoogleMapsDependencyComponent() { if (googleMapsDependencyComponent == null) { googleMapsDependencyComponent = DaggerGoogleMapsDependencyComponent.builder() - .googleMapsDependencyModule(new CollectGoogleMapsDependencyModule( - applicationComponent.referenceLayerRepository(), - applicationComponent.locationClient(), - applicationComponent.settingsProvider() - )) + .googleMapsDependencyModule(new CollectGoogleMapsDependencyModule(applicationComponent)) .build(); } diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGoogleMapsDependencyModule.kt b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGoogleMapsDependencyModule.kt index 3cb17b3f5a8..ff322a6bbb3 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGoogleMapsDependencyModule.kt +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGoogleMapsDependencyModule.kt @@ -6,19 +6,17 @@ import org.odk.collect.maps.layers.ReferenceLayerRepository import org.odk.collect.settings.SettingsProvider class CollectGoogleMapsDependencyModule( - private val referenceLayerRepository: ReferenceLayerRepository, - private val locationClient: LocationClient, - private val settingsProvider: SettingsProvider + private val appDependencyComponent: AppDependencyComponent ) : GoogleMapsDependencyModule() { override fun providesReferenceLayerRepository(): ReferenceLayerRepository { - return referenceLayerRepository + return appDependencyComponent.referenceLayerRepository() } override fun providesLocationClient(): LocationClient { - return locationClient + return appDependencyComponent.locationClient() } override fun providesSettingsProvider(): SettingsProvider { - return settingsProvider + return appDependencyComponent.settingsProvider() } } From 9c97a17a123e701ef8fe5979ec9b543ef3127e26 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 16:27:21 +0200 Subject: [PATCH 621/750] Fixed CollectOsmDroidDependencyModule --- .../org/odk/collect/android/application/Collect.java | 6 +----- .../config/CollectOsmDroidDependencyModule.kt | 12 +++++------- 2 files changed, 6 insertions(+), 12 deletions(-) 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 a7c0d2babc4..043b8f746bf 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 @@ -312,11 +312,7 @@ public GeoDependencyComponent getGeoDependencyComponent() { public OsmDroidDependencyComponent getOsmDroidDependencyComponent() { if (osmDroidDependencyComponent == null) { osmDroidDependencyComponent = DaggerOsmDroidDependencyComponent.builder() - .osmDroidDependencyModule(new CollectOsmDroidDependencyModule( - applicationComponent.referenceLayerRepository(), - applicationComponent.locationClient(), - applicationComponent.settingsProvider() - )) + .osmDroidDependencyModule(new CollectOsmDroidDependencyModule(applicationComponent)) .build(); } diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectOsmDroidDependencyModule.kt b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectOsmDroidDependencyModule.kt index 08dfe459ae9..77630904a55 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectOsmDroidDependencyModule.kt +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectOsmDroidDependencyModule.kt @@ -9,25 +9,23 @@ import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.keys.ProjectKeys class CollectOsmDroidDependencyModule( - private val referenceLayerRepository: ReferenceLayerRepository, - private val locationClient: LocationClient, - private val settingsProvider: SettingsProvider + private val appDependencyComponent: AppDependencyComponent ) : OsmDroidDependencyModule() { override fun providesReferenceLayerRepository(): ReferenceLayerRepository { - return referenceLayerRepository + return appDependencyComponent.referenceLayerRepository() } override fun providesLocationClient(): LocationClient { - return locationClient + return appDependencyComponent.locationClient() } override fun providesMapConfigurator(): MapConfigurator { return MapConfiguratorProvider.getConfigurator( - settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_BASEMAP_SOURCE) + appDependencyComponent.settingsProvider().getUnprotectedSettings().getString(ProjectKeys.KEY_BASEMAP_SOURCE) ) } override fun providesSettingsProvider(): SettingsProvider { - return settingsProvider + return appDependencyComponent.settingsProvider() } } From 6b12afcbde750fcd5c05bb1f82f0ae7824782e08 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 16:27:47 +0200 Subject: [PATCH 622/750] Fixed CollectProjectsDependencyModule --- .../java/org/odk/collect/android/application/Collect.java | 2 +- .../injection/config/CollectProjectsDependencyModule.kt | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) 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 043b8f746bf..7e9ff32d60f 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 @@ -197,7 +197,7 @@ private void setupDagger() { .build(); projectsDependencyComponent = DaggerProjectsDependencyComponent.builder() - .projectsDependencyModule(new CollectProjectsDependencyModule(applicationComponent.projectsRepository())) + .projectsDependencyModule(new CollectProjectsDependencyModule(applicationComponent)) .build(); selfieCameraDependencyComponent = DaggerSelfieCameraDependencyComponent.builder() diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectProjectsDependencyModule.kt b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectProjectsDependencyModule.kt index f14ca0780c4..e9c2fdd374f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectProjectsDependencyModule.kt +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectProjectsDependencyModule.kt @@ -3,9 +3,10 @@ package org.odk.collect.android.injection.config import org.odk.collect.projects.ProjectsDependencyModule import org.odk.collect.projects.ProjectsRepository -class CollectProjectsDependencyModule(private val projectsRepository: ProjectsRepository) : - ProjectsDependencyModule() { +class CollectProjectsDependencyModule( + private val appDependencyComponent: AppDependencyComponent +) : ProjectsDependencyModule() { override fun providesProjectsRepository(): ProjectsRepository { - return projectsRepository + return appDependencyComponent.projectsRepository() } } From c5a3ec5d35ea8f0e2115c36d6863574734c3dd3d Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 16:28:13 +0200 Subject: [PATCH 623/750] Fixed CollectSelfieCameraDependencyModule --- .../java/org/odk/collect/android/application/Collect.java | 2 +- .../injection/config/CollectSelfieCameraDependencyModule.kt | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) 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 7e9ff32d60f..66a6bb22b99 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 @@ -201,7 +201,7 @@ private void setupDagger() { .build(); selfieCameraDependencyComponent = DaggerSelfieCameraDependencyComponent.builder() - .selfieCameraDependencyModule(new CollectSelfieCameraDependencyModule(applicationComponent::permissionsChecker)) + .selfieCameraDependencyModule(new CollectSelfieCameraDependencyModule(applicationComponent)) .build(); drawDependencyComponent = DaggerDrawDependencyComponent.builder() diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectSelfieCameraDependencyModule.kt b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectSelfieCameraDependencyModule.kt index 2ca543ecb67..f4c27f545a8 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectSelfieCameraDependencyModule.kt +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/CollectSelfieCameraDependencyModule.kt @@ -3,8 +3,10 @@ package org.odk.collect.android.injection.config import org.odk.collect.permissions.PermissionsChecker import org.odk.collect.selfiecamera.SelfieCameraDependencyModule -class CollectSelfieCameraDependencyModule(private val permissionsChecker: () -> PermissionsChecker) : SelfieCameraDependencyModule() { +class CollectSelfieCameraDependencyModule( + private val appDependencyComponent: AppDependencyComponent +) : SelfieCameraDependencyModule() { override fun providesPermissionChecker(): PermissionsChecker { - return permissionsChecker() + return appDependencyComponent.permissionsChecker() } } From c157b7793ca4e635bd1afceb790dc5b1d52b8d55 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 16:46:22 +0200 Subject: [PATCH 624/750] Simplified the implementation of DirectoryReferenceLayerRepository --- .../android/injection/config/AppDependencyModule.java | 4 +--- .../maps/layers/DirectoryReferenceLayerRepository.kt | 6 +++--- .../maps/layers/DirectoryReferenceLayerRepositoryTest.kt | 5 +++-- .../maps/layers/MapFragmentReferenceLayerUtilsTest.kt | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) 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 77da92bd5d7..7fa9eaf2a97 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 @@ -574,9 +574,7 @@ public ReferenceLayerRepository providesReferenceLayerRepository(StoragePathProv return new DirectoryReferenceLayerRepository( storagePathProvider.getOdkDirPath(StorageSubdirectory.SHARED_LAYERS), storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS), - () -> MapConfiguratorProvider.getConfigurator( - settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_BASEMAP_SOURCE) - ) + MapConfiguratorProvider.getConfigurator(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_BASEMAP_SOURCE)) ); } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt index 8e5d2aaca14..7646485c575 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt @@ -8,14 +8,14 @@ import java.io.File class DirectoryReferenceLayerRepository( private val sharedLayersDirPath: String, private val projectLayersDirPath: String, - private val getMapConfigurator: () -> MapConfigurator + private val mapConfigurator: MapConfigurator ) : ReferenceLayerRepository { override fun getAll(): List { return getAllFilesWithDirectory() .map { ReferenceLayer(getIdForFile(it.second, it.first), it.first, getName(it.first)) } .distinctBy { it.id } - .filter { getMapConfigurator().supportsLayer(it.file) } + .filter { mapConfigurator.supportsLayer(it.file) } } override fun get(id: String): ReferenceLayer? { @@ -50,6 +50,6 @@ class DirectoryReferenceLayerRepository( PathUtils.getRelativeFilePath(directoryPath, file.absolutePath) private fun getName(file: File): String { - return getMapConfigurator().getDisplayName(file) + return mapConfigurator.getDisplayName(file) } } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index 9529661bc05..12e6b91a75f 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -19,8 +19,9 @@ class DirectoryReferenceLayerRepositoryTest { private val mapConfigurator = StubMapConfigurator() private val repository = DirectoryReferenceLayerRepository( sharedLayersDir.absolutePath, - projectLayersDir.absolutePath - ) { mapConfigurator } + projectLayersDir.absolutePath, + mapConfigurator + ) @Test fun getAll_returnsAllSupportedLayersInTheDirectory() { diff --git a/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt index 36b91bd7f7a..0ba1691a3ca 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt @@ -57,7 +57,7 @@ class MapFragmentReferenceLayerUtilsTest { assertNotNull( MapFragmentReferenceLayerUtils.getReferenceLayerFile( config, - DirectoryReferenceLayerRepository(layersPath, "") { mapConfigurator } + DirectoryReferenceLayerRepository(layersPath, "", mapConfigurator) ) ) } From 524a9eb14fa0da59bd2aa599dc772ac3dee2006f Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 23:33:41 +0200 Subject: [PATCH 625/750] Added failing tests --- .../maps/layers/OfflineMapLayersPickerTest.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index a95cc7d0053..79d107708af 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -195,6 +195,23 @@ class OfflineMapLayersPickerTest { onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.radio_button)).check(matches(isChecked())) } + @Test + fun `when layer id is saved in settings but the layer it belongs to does not exist the 'None' option should be checked`() { + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) + ) + + settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, "2") + + launchFragment() + + scheduler.flush() + + onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(isChecked())) + } + @Test fun `progress indicator is displayed during loading layers`() { launchFragment() @@ -500,6 +517,29 @@ class OfflineMapLayersPickerTest { verify(referenceLayerRepository, never()).delete("2") } + @Test + fun `deleting the selected layer changes selection to 'none' and saves it`() { + val layerFile1 = TempFiles.createTempFile() + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + ReferenceLayer("1", layerFile1, "layer1") + )) + settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, "1") + + launchFragment() + + scheduler.flush() + + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.arrow)).perform(click()) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform(scrollTo(), click()) + + onView(withText(string.delete_layer)).inRoot(isDialog()).perform(click()) + scheduler.flush() + + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(isChecked())) + assertThat(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER), equalTo(null)) + } + @Test fun `progress indicator is displayed during deleting layers`() { val layerFile1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) From 05e737093cca9e616785d43617b8f995b54a33ba Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 23:38:59 +0200 Subject: [PATCH 626/750] Reset to 'none' when selected layer does not exist anymore --- .../odk/collect/maps/layers/OfflineMapLayersPicker.kt | 7 ++++--- .../maps/layers/OfflineMapLayersStateViewModel.kt | 11 ++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 93371897be7..483c98883b8 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -124,6 +124,10 @@ class OfflineMapLayersPicker( ).observe(this) { (layers, checkedLayerId, expandedLayerIds) -> updateAdapter(layers, checkedLayerId, expandedLayerIds, adapter) } + + sharedViewModel.existingLayers.observe(this) { layers -> + stateViewModel.onLayersChanged(layers.map { it.id }.plus(null)) + } } override fun onStart() { @@ -149,9 +153,6 @@ class OfflineMapLayersPicker( MaterialAlertDialogBuilder(requireActivity()) .setMessage(requireActivity().getLocalizedString(org.odk.collect.strings.R.string.delete_layer_confirmation_message, layerItem.name)) .setPositiveButton(org.odk.collect.strings.R.string.delete_layer) { _, _ -> - if (layerItem.id == stateViewModel.getCheckedLayer()) { - stateViewModel.onLayerChecked(null) - } sharedViewModel.deleteLayer(layerItem.id!!) } .setNegativeButton(org.odk.collect.strings.R.string.cancel, null) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt index b54dce05d3a..c6f84407b90 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt @@ -8,7 +8,9 @@ import org.odk.collect.androidshared.livedata.NonNullLiveData import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.keys.ProjectKeys -class OfflineMapLayersStateViewModel(settingsProvider: SettingsProvider) : ViewModel() { +class OfflineMapLayersStateViewModel( + private val settingsProvider: SettingsProvider +) : ViewModel() { private val _expandedLayerIds = MutableNonNullLiveData>(emptyList()) val expandedLayerIds: NonNullLiveData> = _expandedLayerIds @@ -28,6 +30,13 @@ class OfflineMapLayersStateViewModel(settingsProvider: SettingsProvider) : ViewM } } + fun onLayersChanged(layerIds: List) { + if (!layerIds.contains(_checkedLayerId.value)) { + _checkedLayerId.value = null + settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, null) + } + } + fun getCheckedLayer(): String? { return checkedLayerId.value } From 18bc9acb6535b4afa5ef01fc236cabedcaa346eb Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 25 Mar 2024 16:56:59 +0100 Subject: [PATCH 627/750] Fixed updating index --- .../activities/FormFillingActivity.java | 35 +++++++++++------- .../android/formentry/FormEntryViewModel.java | 36 ++++++++++--------- .../formentry/FormIndexAnimationHandler.kt | 6 +--- 3 files changed, 43 insertions(+), 34 deletions(-) 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..f46c1c4cf06 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 @@ -559,8 +559,15 @@ private void setupViewModels(FormEntryViewModelFactory formEntryViewModelFactory formEntryViewModel = viewModelProvider.get(FormEntryViewModel.class); printerWidgetViewModel = viewModelProvider.get(PrinterWidgetViewModel.class); - formEntryViewModel.getCurrentIndex().observe(this, index -> { - formIndexAnimationHandler.handle(index); + formEntryViewModel.getCurrentIndex().observe(this, indexAndValidationResult -> { + if (indexAndValidationResult != null) { + FormIndex formIndex = indexAndValidationResult.component1(); + ValidationResult validationResult = indexAndValidationResult.component2(); + formIndexAnimationHandler.handle(formIndex); + if (validationResult != null) { + handleValidationResult(validationResult); + } + } }); formEntryViewModel.isLoading().observe(this, isLoading -> { @@ -581,16 +588,7 @@ private void setupViewModels(FormEntryViewModelFactory formEntryViewModelFactory return; } ValidationResult validationResult = consumable.getValue(); - if (validationResult instanceof FailedValidationResult failedValidationResult) { - String errorMessage = failedValidationResult.getCustomErrorMessage(); - if (errorMessage == null) { - errorMessage = getString(failedValidationResult.getDefaultErrorMessage()); - } - getCurrentViewIfODKView().setErrorForQuestionWithIndex(failedValidationResult.getIndex(), errorMessage); - swipeHandler.setBeenSwiped(false); - } else if (validationResult instanceof SuccessValidationResult) { - SnackbarUtils.showLongSnackbar(findViewById(R.id.llParent), getString(org.odk.collect.strings.R.string.success_form_validation), findViewById(R.id.buttonholder)); - } + handleValidationResult(validationResult); consumable.consume(); }); @@ -644,6 +642,19 @@ private void setupViewModels(FormEntryViewModelFactory formEntryViewModelFactory }); } + private void handleValidationResult(ValidationResult validationResult) { + if (validationResult instanceof FailedValidationResult failedValidationResult) { + String errorMessage = failedValidationResult.getCustomErrorMessage(); + if (errorMessage == null) { + errorMessage = getString(failedValidationResult.getDefaultErrorMessage()); + } + getCurrentViewIfODKView().setErrorForQuestionWithIndex(failedValidationResult.getIndex(), errorMessage); + swipeHandler.setBeenSwiped(false); + } else if (validationResult instanceof SuccessValidationResult) { + SnackbarUtils.showLongSnackbar(findViewById(R.id.llParent), getString(org.odk.collect.strings.R.string.success_form_validation), findViewById(R.id.buttonholder)); + } + } + private void formControllerAvailable(@NonNull FormController formController, @NonNull Form form, @Nullable Instance instance) { formSessionRepository.set(sessionId, formController, form, instance); AnalyticsUtils.setForm(formController); diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryViewModel.java b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryViewModel.java index 2c9ef16ea91..24343ab59b4 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryViewModel.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryViewModel.java @@ -44,13 +44,15 @@ import java.util.Map; import java.util.function.Supplier; +import kotlin.Pair; + public class FormEntryViewModel extends ViewModel implements SelectChoiceLoader { private final Supplier clock; private final MutableLiveData error = new MutableLiveData<>(null); private final MutableNonNullLiveData hasBackgroundRecording = new MutableNonNullLiveData<>(false); - private final MutableLiveData currentIndex = new MutableLiveData<>(null); + private final MutableLiveData> currentIndex = new MutableLiveData<>(null); private final MutableLiveData> validationResult = new MutableLiveData<>(new Consumable<>(null)); @NonNull @@ -103,7 +105,7 @@ public FormController getFormController() { return formController; } - public LiveData getCurrentIndex() { + public LiveData> getCurrentIndex() { return currentIndex; } @@ -127,7 +129,7 @@ public void promptForNewRepeat() { jumpBackIndex = formController.getFormIndex(); jumpToNewRepeat(); - updateIndex(false); + updateIndex(false, null); } public void jumpToNewRepeat() { @@ -155,7 +157,7 @@ public void addRepeat() { } } - updateIndex(false); + updateIndex(false, null); } public void cancelRepeatPrompt() { @@ -175,7 +177,7 @@ public void cancelRepeatPrompt() { } } - updateIndex(true); + updateIndex(true, null); return null; }, ignored -> {}); } @@ -205,7 +207,7 @@ public void moveForward(HashMap answers, Boolean evaluat try { formController.stepToNextScreenEvent(); formController.getAuditEventLogger().flush(); // Close events waiting for an end time - updateIndex(true); + updateIndex(true, null); } catch (JavaRosaException e) { error.postValue(new FormError.NonFatal(e.getCause().getMessage())); } @@ -224,7 +226,7 @@ public void moveBackward(HashMap answers) { try { formController.stepToPreviousScreenEvent(); formController.getAuditEventLogger().flush(); // Close events waiting for an end time - updateIndex(true); + updateIndex(true, null); } catch (JavaRosaException e) { error.postValue(new FormError.NonFatal(e.getCause().getMessage())); } @@ -308,17 +310,17 @@ protected void onCleared() { */ @Deprecated public void refreshSync() { - updateIndex(false); + updateIndex(false, null); } public void refresh() { worker.immediate((Supplier) () -> { - updateIndex(true); + updateIndex(true, null); return null; }, ignored -> {}); } - private void updateIndex(boolean isAsync) { + private void updateIndex(boolean isAsync, @Nullable FailedValidationResult validationResult) { choices.clear(); if (formController != null) { @@ -348,9 +350,9 @@ private void updateIndex(boolean isAsync) { AuditUtils.logCurrentScreen(formController, formController.getAuditEventLogger(), clock.get()); if (isAsync) { - currentIndex.postValue(formController.getFormIndex()); + currentIndex.postValue(new Pair<>(formController.getFormIndex(), validationResult)); } else { - currentIndex.setValue(formController.getFormIndex()); + currentIndex.setValue(new Pair<>(formController.getFormIndex(), validationResult)); } } } @@ -372,13 +374,13 @@ public void validate() { // JavaRosa moves to the index where the contraint failed if (result instanceof FailedValidationResult) { - updateIndex(true); + updateIndex(true, (FailedValidationResult) result); + } else { + validationResult.postValue(new Consumable<>(result)); } - return result; - }, result -> { - validationResult.setValue(new Consumable<>(result)); - } + return null; + }, ignored -> {} ); } diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/FormIndexAnimationHandler.kt b/collect_app/src/main/java/org/odk/collect/android/formentry/FormIndexAnimationHandler.kt index 2af7b0f3e9f..069c3c694f2 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/FormIndexAnimationHandler.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/FormIndexAnimationHandler.kt @@ -14,11 +14,7 @@ class FormIndexAnimationHandler(private val listener: Listener) { private var lastIndex: FormIndex? = null - fun handle(index: FormIndex?) { - if (index == null) { - return - } - + fun handle(index: FormIndex) { if (lastIndex == null) { listener.onScreenRefresh() } else { From f9c0062218d9137fc743442f4f2927032a14afab Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 14 Jun 2024 12:29:46 +0200 Subject: [PATCH 628/750] Moved checking if layer is supported from MapsPreferencesFragment to ReferenceLayerRepository --- .../preferences/screens/MapsPreferencesFragment.kt | 4 ++-- .../maps/layers/DirectoryReferenceLayerRepository.kt | 8 ++++++++ .../odk/collect/maps/layers/ReferenceLayerRepository.kt | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt index 61ca8b776dd..eb6e9013212 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt @@ -151,8 +151,8 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP // Clear the reference layer if it does not exist or it isn't supported by the new basemap. val layerId = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) if (layerId != null) { - val layer = referenceLayerRepository.get(layerId) - if (layer == null || !cftor.supportsLayer(layer.file)) { + val layer = referenceLayerRepository.getSupported(layerId) + if (layer == null) { settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, null) } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt index 7646485c575..83d3ec4c6df 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt @@ -28,6 +28,14 @@ class DirectoryReferenceLayerRepository( } } + override fun getSupported(id: String): ReferenceLayer? { + val layer = get(id) + if (layer != null && mapConfigurator.supportsLayer(layer.file)) { + return layer + } + return null + } + override fun addLayer(file: File, shared: Boolean) { if (shared) { file.copyTo(File(sharedLayersDirPath, file.name), true) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt index a7520708d7e..5248fe83840 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt @@ -6,6 +6,7 @@ interface ReferenceLayerRepository { fun getAll(): List fun get(id: String): ReferenceLayer? + fun getSupported(id: String): ReferenceLayer? fun addLayer(file: File, shared: Boolean) fun delete(id: String) } From 77513506ab222c37b45917174327e028f264de7d Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 14 Jun 2024 12:52:02 +0200 Subject: [PATCH 629/750] Added a failing test --- .../screens/MapsPreferencesFragmentTest.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt b/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt index 778f16d725f..d9cfb991ae8 100644 --- a/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt @@ -1,6 +1,7 @@ package org.odk.collect.android.preferences.screens import android.content.Context +import androidx.preference.Preference import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.gson.Gson import org.hamcrest.CoreMatchers.equalTo @@ -15,13 +16,17 @@ import org.odk.collect.android.application.initialization.AnalyticsInitializer import org.odk.collect.android.application.initialization.MapsInitializer import org.odk.collect.android.injection.config.AppDependencyModule import org.odk.collect.android.projects.ProjectsDataService +import org.odk.collect.android.storage.StoragePathProvider import org.odk.collect.android.support.CollectHelpers import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule +import org.odk.collect.maps.layers.ReferenceLayer +import org.odk.collect.maps.layers.ReferenceLayerRepository import org.odk.collect.projects.Project import org.odk.collect.projects.ProjectsRepository import org.odk.collect.settings.InMemSettingsProvider import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.keys.ProjectKeys +import org.odk.collect.shared.TempFiles import org.odk.collect.shared.strings.UUIDGenerator @RunWith(AndroidJUnit4::class) @@ -37,6 +42,7 @@ class MapsPreferencesFragmentTest { private val projectsRepository = mock().apply { whenever(get(project.uuid)).thenReturn(project) } + private val referenceLayerRepository = mock() private val settingsProvider = InMemSettingsProvider() @Before @@ -63,6 +69,13 @@ class MapsPreferencesFragmentTest { override fun providesSettingsProvider(context: Context): SettingsProvider { return settingsProvider } + + override fun providesReferenceLayerRepository( + storagePathProvider: StoragePathProvider, + settingsProvider: SettingsProvider + ): ReferenceLayerRepository { + return referenceLayerRepository + } }) } @@ -75,4 +88,22 @@ class MapsPreferencesFragmentTest { assertThat(settings.getString(ProjectKeys.KEY_REFERENCE_LAYER), equalTo(null)) } + + @Test + fun `if saved layer exist its name is displayed`() { + val settings = settingsProvider.getUnprotectedSettings() + settings.save(ProjectKeys.KEY_REFERENCE_LAYER, "blah") + val layer = ReferenceLayer("blah", TempFiles.createTempFile(), "blah") + whenever(referenceLayerRepository.getSupported("blah")).thenReturn(layer) + whenever(referenceLayerRepository.get("blah")).thenReturn(layer) + + val scenario = launcherRule.launch(MapsPreferencesFragment::class.java) + + scenario.onFragment { + assertThat( + it.findPreference(ProjectKeys.KEY_REFERENCE_LAYER)!!.summary, + equalTo("blah") + ) + } + } } From feff3339b1e546e1313b22b5d4eeda3368227e5c Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 14 Jun 2024 12:52:21 +0200 Subject: [PATCH 630/750] Removed the redundant line --- .../android/preferences/screens/MapsPreferencesFragment.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt index eb6e9013212..7923ab54bf9 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt @@ -155,8 +155,6 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP if (layer == null) { settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, null) } - - settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, null) } } } From 417055fe7ac1bfb1c319966b0245e803d231f1d2 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 14 Jun 2024 13:05:36 +0200 Subject: [PATCH 631/750] Removed #getSupported and let #get check if layer is supported to match what #getAll does --- .../preferences/screens/MapsPreferencesFragment.kt | 2 +- .../preferences/screens/MapsPreferencesFragmentTest.kt | 1 - .../maps/layers/DirectoryReferenceLayerRepository.kt | 10 +--------- .../collect/maps/layers/ReferenceLayerRepository.kt | 1 - 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt index 7923ab54bf9..c9a3a4fe31f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt @@ -151,7 +151,7 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP // Clear the reference layer if it does not exist or it isn't supported by the new basemap. val layerId = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) if (layerId != null) { - val layer = referenceLayerRepository.getSupported(layerId) + val layer = referenceLayerRepository.get(layerId) if (layer == null) { settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, null) } diff --git a/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt b/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt index d9cfb991ae8..3a345d4b137 100644 --- a/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt @@ -94,7 +94,6 @@ class MapsPreferencesFragmentTest { val settings = settingsProvider.getUnprotectedSettings() settings.save(ProjectKeys.KEY_REFERENCE_LAYER, "blah") val layer = ReferenceLayer("blah", TempFiles.createTempFile(), "blah") - whenever(referenceLayerRepository.getSupported("blah")).thenReturn(layer) whenever(referenceLayerRepository.get("blah")).thenReturn(layer) val scenario = launcherRule.launch(MapsPreferencesFragment::class.java) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt index 83d3ec4c6df..04735c74d73 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt @@ -21,21 +21,13 @@ class DirectoryReferenceLayerRepository( override fun get(id: String): ReferenceLayer? { val file = getAllFilesWithDirectory().firstOrNull { getIdForFile(it.second, it.first) == id } - return if (file != null) { + return if (file != null && mapConfigurator.supportsLayer(file.first)) { ReferenceLayer(getIdForFile(file.second, file.first), file.first, getName(file.first)) } else { null } } - override fun getSupported(id: String): ReferenceLayer? { - val layer = get(id) - if (layer != null && mapConfigurator.supportsLayer(layer.file)) { - return layer - } - return null - } - override fun addLayer(file: File, shared: Boolean) { if (shared) { file.copyTo(File(sharedLayersDirPath, file.name), true) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt index 5248fe83840..a7520708d7e 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt @@ -6,7 +6,6 @@ interface ReferenceLayerRepository { fun getAll(): List fun get(id: String): ReferenceLayer? - fun getSupported(id: String): ReferenceLayer? fun addLayer(file: File, shared: Boolean) fun delete(id: String) } From 4f070335fd0e827f157f5652fd17f4812729d8ca Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 14 Jun 2024 13:32:21 +0200 Subject: [PATCH 632/750] Improved tests --- .../layers/DirectoryReferenceLayerRepository.kt | 2 +- .../layers/DirectoryReferenceLayerRepositoryTest.kt | 13 ++++++++++++- .../layers/MapFragmentReferenceLayerUtilsTest.kt | 5 ++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt index 04735c74d73..06c3cf8e57d 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt @@ -46,7 +46,7 @@ class DirectoryReferenceLayerRepository( } } - private fun getIdForFile(directoryPath: String, file: File) = + fun getIdForFile(directoryPath: String, file: File) = PathUtils.getRelativeFilePath(directoryPath, file.absolutePath) private fun getName(file: File): String { diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index 12e6b91a75f..fc0275c7e96 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -86,7 +86,7 @@ class DirectoryReferenceLayerRepositoryTest { } @Test - fun get_returnsLayer() { + fun get_returnsProperLayer() { val file1 = TempFiles.createTempFile(sharedLayersDir) val file2 = TempFiles.createTempFile(sharedLayersDir) mapConfigurator.apply { @@ -98,6 +98,17 @@ class DirectoryReferenceLayerRepositoryTest { assertThat(repository.get(file2Layer.id)!!.file, equalTo(file2)) } + @Test + fun get_returnsNullIfLayerIsNotSupported() { + val file = TempFiles.createTempFile(sharedLayersDir) + mapConfigurator.apply { + addFile(file, false, file.name) + } + + val fileId = repository.getIdForFile(sharedLayersDir.absolutePath, file) + assertThat(repository.get(fileId), equalTo(null)) + } + @Test fun get_withMultipleDirectories_returnsLayer() { val file1 = TempFiles.createTempFile(sharedLayersDir) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt index 0ba1691a3ca..389c24da318 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt @@ -45,12 +45,15 @@ class MapFragmentReferenceLayerUtilsTest { @Test fun whenOfflineLayerFileExist_should_getReferenceLayerFileReturnThatFile() { val layersPath = createTempDir().absolutePath - File(layersPath, "blah").writeBytes(byteArrayOf()) + val file = File(layersPath, "blah").also { + it.writeBytes(byteArrayOf()) + } val config = Bundle() config.putString(MapFragment.KEY_REFERENCE_LAYER, "blah") val mapConfigurator = mock().also { + whenever(it.supportsLayer(file)).thenReturn(true) whenever(it.getDisplayName(File(layersPath, "blah"))).thenReturn("blah") } From 94a1cf8ecc2b9f7849a70f93678a76807461eab9 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 14 Jun 2024 15:52:12 +0200 Subject: [PATCH 633/750] Added failing tests --- .../geo/geopoint/GeoPointMapActivityTest.java | 16 +++++++++++++++- .../collect/geo/geopoly/GeoPolyActivityTest.kt | 13 ++++++++++++- .../odk/collect/geo/support/FakeMapFragment.kt | 4 ++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.java b/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.java index 21b967e0312..3ab36f9f2bd 100644 --- a/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.java +++ b/geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.java @@ -1,6 +1,9 @@ package org.odk.collect.geo.geopoint; import static android.app.Activity.RESULT_OK; +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.matcher.ViewMatchers.withId; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -32,6 +35,7 @@ import org.odk.collect.maps.MapFragmentFactory; import org.odk.collect.maps.MapPoint; import org.odk.collect.maps.layers.ReferenceLayerRepository; +import org.odk.collect.settings.InMemSettingsProvider; import org.odk.collect.settings.SettingsProvider; import org.odk.collect.webpage.ExternalWebPageHelper; import org.robolectric.shadows.ShadowApplication; @@ -77,7 +81,7 @@ public Scheduler providesScheduler() { @NonNull @Override public SettingsProvider providesSettingsProvider() { - return mock(); + return new InMemSettingsProvider(); } @NonNull @@ -165,4 +169,14 @@ public void passingRetainMockAccuracyExtra_showSetItOnLocationClient() { assertThat(mapFragment.isRetainMockAccuracy(), is(false)); } + + @Test + public void recreatingTheActivityWithTheLayersDialogDisplayedDoesNotCrashTheApp() { + ActivityScenario scenario = launcherRule.launch(GeoPointMapActivity.class); + mapFragment.ready(); + + onView(withId(R.id.layer_menu)).perform(click()); + + scenario.recreate(); + } } diff --git a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyActivityTest.kt b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyActivityTest.kt index dc4d62d4f94..bdf2cbc061d 100644 --- a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyActivityTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyActivityTest.kt @@ -37,6 +37,7 @@ import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapFragmentFactory import org.odk.collect.maps.MapPoint import org.odk.collect.maps.layers.ReferenceLayerRepository +import org.odk.collect.settings.InMemSettingsProvider import org.odk.collect.settings.SettingsProvider import org.odk.collect.webpage.ExternalWebPageHelper import org.robolectric.Shadows @@ -79,7 +80,7 @@ class GeoPolyActivityTest { } override fun providesSettingsProvider(): SettingsProvider { - return mock() + return InMemSettingsProvider() } override fun providesExternalWebPageHelper(): ExternalWebPageHelper { @@ -282,6 +283,16 @@ class GeoPolyActivityTest { assertThat(mapFragment.isPolyDraggable(0), equalTo(false)) } + @Test + fun recreatingTheActivityWithTheLayersDialogDisplayedDoesNotCrashTheApp() { + val scenario = launcherRule.launch(GeoPolyActivity::class.java) + mapFragment.ready() + + onView(withId(R.id.layers)).perform(click()) + + scenario.recreate() + } + private fun startInput(mode: Int) { onView(withId(R.id.play)).perform(click()) onView(withId(mode)).inRoot(isDialog()).perform(click()) diff --git a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt index 914c982b342..397ece4b54e 100644 --- a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt +++ b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt @@ -103,8 +103,8 @@ class FakeMapFragment : Fragment(), MapFragment { markerIcons[featureId] = markerIconDescription } - override fun getMarkerPoint(featureId: Int): MapPoint { - return markers[featureId]!! + override fun getMarkerPoint(featureId: Int): MapPoint? { + return markers[featureId] } override fun addPolyLine(lineDescription: LineDescription): Int { From c60e4e2d42ce60cd8db1b193dc723098073e51e1 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 14 Jun 2024 15:52:30 +0200 Subject: [PATCH 634/750] Fixed setting fragment factories --- .../org/odk/collect/geo/geopoint/GeoPointMapActivity.java | 6 ++---- .../java/org/odk/collect/geo/geopoly/GeoPolyActivity.java | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java index 071e441d7e1..c10e981b998 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java +++ b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java @@ -137,20 +137,18 @@ public class GeoPointMapActivity extends LocalizedActivity { @Override public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - + ((GeoDependencyComponentProvider) getApplication()).getGeoDependencyComponent().inject(this); getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() .forClass(MapFragment.class, () -> (Fragment) mapFragmentFactory.createMapFragment()) .forClass(OfflineMapLayersPicker.class, () -> new OfflineMapLayersPicker(getActivityResultRegistry(), referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper)) .build() ); + super.onCreate(savedInstanceState); requireLocationPermissions(this); previousState = savedInstanceState; - ((GeoDependencyComponentProvider) getApplication()).getGeoDependencyComponent().inject(this); - requestWindowFeature(Window.FEATURE_NO_TITLE); try { setContentView(R.layout.geopoint_layout); diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java index ae842ddf123..487ef0158f5 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java @@ -150,7 +150,7 @@ public void handleOnBackPressed() { }; @Override public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + ((GeoDependencyComponentProvider) getApplication()).getGeoDependencyComponent().inject(this); getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() .forClass(MapFragment.class, () -> (Fragment) mapFragmentFactory.createMapFragment()) @@ -158,12 +158,12 @@ public void handleOnBackPressed() { .build() ); + super.onCreate(savedInstanceState); + requireLocationPermissions(this); previousState = savedInstanceState; - ((GeoDependencyComponentProvider) getApplication()).getGeoDependencyComponent().inject(this); - if (savedInstanceState != null) { restoredPoints = savedInstanceState.getParcelableArrayList(POINTS_KEY); inputActive = savedInstanceState.getBoolean(INPUT_ACTIVE_KEY, false); From 8d359e1a9fa8eb620b57aa2da953f2984f32451a Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 12:49:53 +0200 Subject: [PATCH 635/750] Sort displayed layers --- .../maps/layers/OfflineMapLayersPicker.kt | 20 ++++++++++--------- .../maps/layers/OfflineMapLayersViewModel.kt | 2 +- .../layers/OfflineMapLayersImporterTest.kt | 8 ++++---- .../maps/layers/OfflineMapLayersPickerTest.kt | 10 +++++----- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 483c98883b8..f159b81a747 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -180,15 +180,17 @@ class OfflineMapLayersPicker( ) ) - newData.addAll(layers.map { - CheckableReferenceLayer( - it.id, - it.file, - it.name, - checkedLayerId == it.id, - expandedLayerIds.contains(it.id) - ) - }) + newData.addAll( + layers.map { + CheckableReferenceLayer( + it.id, + it.file, + it.name, + checkedLayerId == it.id, + expandedLayerIds.contains(it.id) + ) + }.sortedBy { it.name } + ) adapter.setData(newData) } } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index 893104dc060..2d767a21213 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -65,7 +65,7 @@ class OfflineMapLayersViewModel( } } _isLoading.postValue(false) - _layersToImport.postValue(layers) + _layersToImport.postValue(layers.sortedBy { it.name }) }, foreground = { } ) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt index f304774db04..5ca8dd8fe52 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt @@ -145,9 +145,9 @@ class OfflineMapLayersImporterTest { } @Test - fun `the list of selected layers should be displayed`() { - val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) - val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) + fun `the list of selected layers should be displayed in A-Z order`() { + val file1 = TempFiles.createTempFile("layerB", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layerA", MbtilesFile.FILE_EXTENSION) launchFragment().onFragment { it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext()) @@ -156,8 +156,8 @@ class OfflineMapLayersImporterTest { scheduler.flush() onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) - onView(withText(file1.name)).check(matches(isDisplayed())) onView(withText(file2.name)).check(matches(isDisplayed())) + onView(withText(file1.name)).check(matches(isDisplayed())) } @Test diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index 79d107708af..39afb16e76e 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -254,11 +254,11 @@ class OfflineMapLayersPickerTest { } @Test - fun `if there are multiple layers all of them are displayed along with the 'None'`() { + fun `if there are multiple layers all of them are displayed along with the 'None' and sorted in A-Z order`() { whenever(referenceLayerRepository.getAll()).thenReturn( listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), - ReferenceLayer("2", TempFiles.createTempFile(), "layer2") + ReferenceLayer("1", TempFiles.createTempFile(), "layerB"), + ReferenceLayer("2", TempFiles.createTempFile(), "layerA") ) ) @@ -268,8 +268,8 @@ class OfflineMapLayersPickerTest { onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3))) onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.title)).check(matches(withText("layer1"))) - onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.title)).check(matches(withText("layer2"))) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.title)).check(matches(withText("layerA"))) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.title)).check(matches(withText("layerB"))) } @Test From f0601926afaacdedf2041afa7e03c361a5533a39 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 12:54:35 +0200 Subject: [PATCH 636/750] Fixed strings --- strings/src/main/res/values/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index e2b882d5d42..fbc1a138feb 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -495,7 +495,7 @@ Learn more about adding MBTiles. - Add layer + Add layers Delete layer @@ -504,8 +504,8 @@ Layers - Select layer access - Do you want the layer available in all projects or your current project only? + Select layers access + Do you want the layers available in all projects or your current project only? All projects @@ -514,7 +514,7 @@ Current project only - Are you sure you want to delete %1$s offline layers? + Are you sure you want to delete %1$s offline layer? Record a point Accuracy: %1$s m From 478a9d03a3c4f495d6dbb59ad5a06058e1287ea3 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 12:57:41 +0200 Subject: [PATCH 637/750] Improved the confirmation layout --- .../layout/offline_map_layers_importer.xml | 212 +++++++++--------- 1 file changed, 108 insertions(+), 104 deletions(-) diff --git a/maps/src/main/res/layout/offline_map_layers_importer.xml b/maps/src/main/res/layout/offline_map_layers_importer.xml index 2b678de5f32..761f7c3b553 100644 --- a/maps/src/main/res/layout/offline_map_layers_importer.xml +++ b/maps/src/main/res/layout/offline_map_layers_importer.xml @@ -5,7 +5,7 @@ android:layout_height="match_parent"> - + app:layout_constraintEnd_toEndOf="parent" /> - + + + + + + + + + + + + + - - - - - - - - - - - - + + + android:text="@string/all_projects_option"/> - - - - - - - - - - + + + + From 8c1765732a504029a1d1a270376c8c191ec481d7 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 13:49:03 +0200 Subject: [PATCH 638/750] Removed redundant padding --- maps/src/main/res/layout/offline_map_layers_picker_item.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/maps/src/main/res/layout/offline_map_layers_picker_item.xml b/maps/src/main/res/layout/offline_map_layers_picker_item.xml index 60c63973061..cea34779819 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker_item.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker_item.xml @@ -10,7 +10,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:button="@drawable/radio_button_inset" - android:paddingEnd="@dimen/margin_standard" android:paddingVertical="@dimen/margin_extra_small" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> From a1b5a851ec9cc3b66155d641993c73b8b664b984 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 13 Jun 2024 14:40:50 +0200 Subject: [PATCH 639/750] Fixed tests --- .../maps/layers/OfflineMapLayersImporterTest.kt | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt index 5ca8dd8fe52..a3ae6e91369 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt @@ -4,8 +4,6 @@ import androidx.core.net.toUri import androidx.fragment.app.testing.FragmentScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.scrollTo -import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed @@ -31,6 +29,7 @@ import org.odk.collect.strings.R import org.odk.collect.testshared.EspressoHelpers import org.odk.collect.testshared.FakeScheduler import org.odk.collect.testshared.RecyclerViewMatcher +import org.odk.collect.testshared.RecyclerViewMatcher.Companion.withRecyclerView import org.odk.collect.testshared.RobolectricHelpers import java.io.File @@ -156,8 +155,8 @@ class OfflineMapLayersImporterTest { scheduler.flush() onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) - onView(withText(file2.name)).check(matches(isDisplayed())) - onView(withText(file1.name)).check(matches(isDisplayed())) + onView(withRecyclerView(org.odk.collect.maps.R.id.layers).atPositionOnView(0, org.odk.collect.maps.R.id.layer_name)).check(matches(withText(file2.name))) + onView(withRecyclerView(org.odk.collect.maps.R.id.layers).atPositionOnView(1, org.odk.collect.maps.R.id.layer_name)).check(matches(withText(file1.name))) } @Test @@ -174,8 +173,8 @@ class OfflineMapLayersImporterTest { scenario.recreate() onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) - onView(withText(file1.name)).check(matches(isDisplayed())) - onView(withText(file2.name)).check(matches(isDisplayed())) + onView(withRecyclerView(org.odk.collect.maps.R.id.layers).atPositionOnView(0, org.odk.collect.maps.R.id.layer_name)).check(matches(withText(file1.name))) + onView(withRecyclerView(org.odk.collect.maps.R.id.layers).atPositionOnView(1, org.odk.collect.maps.R.id.layer_name)).check(matches(withText(file2.name))) } @Test @@ -190,8 +189,7 @@ class OfflineMapLayersImporterTest { scheduler.flush() onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(1))) - onView(withText(file1.name)).check(matches(isDisplayed())) - onView(withText(file2.name)).check(doesNotExist()) + onView(withRecyclerView(org.odk.collect.maps.R.id.layers).atPositionOnView(0, org.odk.collect.maps.R.id.layer_name)).check(matches(withText(file1.name))) } @Test @@ -229,7 +227,7 @@ class OfflineMapLayersImporterTest { scheduler.flush() - onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(scrollTo(), click()) + onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(click()) onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) scheduler.flush() From 0e133d9a9275b7b226a3e313ac7faad71127a932 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 14 Jun 2024 12:09:24 +0200 Subject: [PATCH 640/750] Moved sorting layers to viewmodel --- .../java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt | 2 +- .../org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index f159b81a747..ce3bcf24d2c 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -189,7 +189,7 @@ class OfflineMapLayersPicker( checkedLayerId == it.id, expandedLayerIds.contains(it.id) ) - }.sortedBy { it.name } + } ) adapter.setData(newData) } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index 2d767a21213..1674d8f503e 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -38,7 +38,7 @@ class OfflineMapLayersViewModel( _isLoading.value = true scheduler.immediate( background = { - val layers = referenceLayerRepository.getAll() + val layers = referenceLayerRepository.getAll().sortedBy { it.name } _isLoading.postValue(false) _existingLayers.postValue(layers) }, From 549563c27743d2490984d4f6cc5c6e1f58f9ada4 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 14 Jun 2024 20:34:26 +0200 Subject: [PATCH 641/750] Added a new test --- .../maps/layers/OfflineMapLayersPickerTest.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index 39afb16e76e..8704b0b0d44 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -540,6 +540,31 @@ class OfflineMapLayersPickerTest { assertThat(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER), equalTo(null)) } + @Test + fun `deleting one of the layers keeps the list sorted in A-Z order`() { + whenever(referenceLayerRepository.getAll()).thenReturn(listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layerC"), + ReferenceLayer("2", TempFiles.createTempFile(), "layerB"), + ReferenceLayer("3", TempFiles.createTempFile(), "layerA") + )) + + launchFragment() + + scheduler.flush() + + onView(withId(R.id.layers)).perform(scrollToPosition(2)) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.arrow)).perform(click()) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.delete_layer)).perform(scrollTo(), click()) + + onView(withText(string.delete_layer)).inRoot(isDialog()).perform(click()) + scheduler.flush() + + onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.title)).check(matches(withText("layerA"))) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.title)).check(matches(withText("layerC"))) + } + @Test fun `progress indicator is displayed during deleting layers`() { val layerFile1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) From 10dcf75fdba20fed0306dc913403bdae10cc79a6 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 17 Jun 2024 13:11:14 +0100 Subject: [PATCH 642/750] Block double clicks on multi select action instead of multi clicks --- .../DoubleClickSafeMaterialButton.kt | 24 +++++++++++++++++++ .../ui/multiclicksafe/MultiClickGuard.kt | 10 ++++---- .../layout/multi_select_controls_layout.xml | 2 +- 3 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 androidshared/src/main/java/org/odk/collect/androidshared/ui/multiclicksafe/DoubleClickSafeMaterialButton.kt diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/ui/multiclicksafe/DoubleClickSafeMaterialButton.kt b/androidshared/src/main/java/org/odk/collect/androidshared/ui/multiclicksafe/DoubleClickSafeMaterialButton.kt new file mode 100644 index 00000000000..d497a82e530 --- /dev/null +++ b/androidshared/src/main/java/org/odk/collect/androidshared/ui/multiclicksafe/DoubleClickSafeMaterialButton.kt @@ -0,0 +1,24 @@ +package org.odk.collect.androidshared.ui.multiclicksafe + +import android.content.Context +import android.util.AttributeSet +import com.google.android.material.button.MaterialButton + +class DoubleClickSafeMaterialButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MaterialButton(context, attrs, defStyleAttr) { + + override fun performClick(): Boolean { + return allowClick() && super.performClick() + } + + /** + * Use [MultiClickGuard] with a scope unique to this object (class name + hash). + */ + private fun allowClick(): Boolean { + val scope = javaClass.name + hashCode() + return MultiClickGuard.allowClick(scope) + } +} diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/ui/multiclicksafe/MultiClickGuard.kt b/androidshared/src/main/java/org/odk/collect/androidshared/ui/multiclicksafe/MultiClickGuard.kt index bf5e0bf3efd..5bc1ffb6fca 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/ui/multiclicksafe/MultiClickGuard.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/ui/multiclicksafe/MultiClickGuard.kt @@ -15,21 +15,21 @@ object MultiClickGuard { } /** - * Debounce multiple clicks within the same screen + * Debounce multiple clicks within the same scope * - * @param screenName The name of the screen. If not provided, the Java class name of the element + * @param scope If not provided, the Java class name of the element * is used. However, this approach is imperfect, as elements on the same screen might belong to * different classes. Consequently, clicks on these elements are treated as interactions occurring * on two distinct screens, not protecting from rapid clicking. */ @JvmStatic @JvmOverloads - fun allowClick(screenName: String = javaClass.name, clickDebounceMs: Long = 1000): Boolean { + fun allowClick(scope: String = javaClass.name, clickDebounceMs: Long = 1000): Boolean { if (test) { return true } val elapsedRealtime = SystemClock.elapsedRealtime() - val isSameClass = screenName == lastClickName + val isSameClass = scope == lastClickName val isBeyondThreshold = elapsedRealtime - lastClickTime > clickDebounceMs val isBeyondTestThreshold = lastClickTime == 0L || lastClickTime == elapsedRealtime // just for tests @@ -38,7 +38,7 @@ object MultiClickGuard { if (allowClick) { lastClickTime = elapsedRealtime - lastClickName = screenName + lastClickName = scope } return allowClick } diff --git a/lists/src/main/res/layout/multi_select_controls_layout.xml b/lists/src/main/res/layout/multi_select_controls_layout.xml index 868ed9e5d4c..3b319d0b603 100644 --- a/lists/src/main/res/layout/multi_select_controls_layout.xml +++ b/lists/src/main/res/layout/multi_select_controls_layout.xml @@ -19,7 +19,7 @@ android:layout_weight="1" android:text="@string/select_all" /> - Date: Mon, 17 Jun 2024 17:23:39 +0100 Subject: [PATCH 643/750] Fix rotation for readonly polygons in OSM --- .../collect/osmdroid/OsmDroidMapFragment.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index 0addebaabf6..eb081b9cc70 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -361,8 +361,8 @@ public void appendPointToPolyLine(int featureId, @NonNull MapPoint point) { public @NonNull List getPolyLinePoints(int featureId) { MapFeature feature = features.get(featureId); - if (feature instanceof DynamicPolyLineFeature) { - return ((DynamicPolyLineFeature) feature).getPoints(); + if (feature instanceof LineFeature) { + return ((LineFeature) feature).getPoints(); } return new ArrayList<>(); } @@ -792,13 +792,19 @@ public void dispose() { } } + private interface LineFeature extends MapFeature { + + List getPoints(); + } + /** * A polyline or polygon that can be manipulated by dragging markers at its vertices. */ - private class StaticPolyLineFeature implements MapFeature { + private class StaticPolyLineFeature implements LineFeature { final MapView map; final Polyline polyline; final boolean closedPolygon; + private final List points; StaticPolyLineFeature(MapView map, LineDescription lineDescription) { this.map = map; @@ -817,7 +823,8 @@ private class StaticPolyLineFeature implements MapFeature { paint.setStrokeWidth(lineDescription.getStrokeWidth()); map.getOverlays().add(polyline); - List geoPoints = StreamSupport.stream(lineDescription.getPoints().spliterator(), false).map(mapPoint -> new GeoPoint(mapPoint.latitude, mapPoint.longitude, mapPoint.altitude)).collect(Collectors.toList()); + points = lineDescription.getPoints(); + List geoPoints = StreamSupport.stream(points.spliterator(), false).map(mapPoint -> new GeoPoint(mapPoint.latitude, mapPoint.longitude, mapPoint.altitude)).collect(Collectors.toList()); if (closedPolygon && !geoPoints.isEmpty()) { geoPoints.add(geoPoints.get(0)); } @@ -848,12 +855,17 @@ public void update() { public void dispose() { map.getOverlays().remove(polyline); } + + @Override + public List getPoints() { + return points; + } } /** * A polyline or polygon that can be manipulated by dragging markers at its vertices. */ - private class DynamicPolyLineFeature implements MapFeature { + private class DynamicPolyLineFeature implements LineFeature { final MapView map; final List markers = new ArrayList<>(); final Polyline polyline; @@ -918,6 +930,7 @@ public void dispose() { map.getOverlays().remove(polyline); } + @Override public List getPoints() { List points = new ArrayList<>(); for (Marker marker : markers) { From bfce1d4c8588c5c16a2bda5db94f25f05a56c6dd Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 18 Jun 2024 14:00:57 +0100 Subject: [PATCH 644/750] Fix rotation for read only lines in all map engines --- .../collect/googlemaps/GoogleMapFragment.java | 23 +++++++++++++++---- .../collect/mapbox/DynamicPolyLineFeature.kt | 16 ++++++------- .../org/odk/collect/mapbox/LineFeature.kt | 7 ++++++ .../odk/collect/mapbox/MapboxMapFragment.kt | 4 ++-- .../collect/mapbox/StaticPolyLineFeature.kt | 10 ++++---- 5 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 mapbox/src/main/java/org/odk/collect/mapbox/LineFeature.kt diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java index a078a5a431c..e2c62e6cc59 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java @@ -330,9 +330,10 @@ public int addPolygon(PolygonDescription polygonDescription) { @Override public @NonNull List getPolyLinePoints(int featureId) { MapFeature feature = features.get(featureId); - if (feature instanceof DynamicPolyLineFeature) { - return ((DynamicPolyLineFeature) feature).getPoints(); + if (feature instanceof LineFeature) { + return ((LineFeature) feature).getPoints(); } + return new ArrayList<>(); } @@ -771,9 +772,15 @@ public void dispose() { } } + private interface LineFeature extends MapFeature { + + List getPoints(); + } + /** A polyline or polygon that can not be manipulated by dragging markers at its vertices. */ - private static class StaticPolyLineFeature implements MapFeature { + private static class StaticPolyLineFeature implements LineFeature { + private List points; private Polyline polyline; StaticPolyLineFeature(LineDescription lineDescription, GoogleMap map) { @@ -781,7 +788,8 @@ private static class StaticPolyLineFeature implements MapFeature { return; } - List latLngs = StreamSupport.stream(lineDescription.getPoints().spliterator(), false).map(mapPoint -> new LatLng(mapPoint.latitude, mapPoint.longitude)).collect(Collectors.toList()); + points = lineDescription.getPoints(); + List latLngs = StreamSupport.stream(points.spliterator(), false).map(mapPoint -> new LatLng(mapPoint.latitude, mapPoint.longitude)).collect(Collectors.toList()); if (lineDescription.getClosed() && !latLngs.isEmpty()) { latLngs.add(latLngs.get(0)); } @@ -830,10 +838,15 @@ private void clearPolyline() { polyline = null; } } + + @Override + public List getPoints() { + return points; + } } /** A polyline or polygon that can be manipulated by dragging markers at its vertices. */ - private static class DynamicPolyLineFeature implements MapFeature { + private static class DynamicPolyLineFeature implements LineFeature { private final Context context; private final GoogleMap map; diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/DynamicPolyLineFeature.kt b/mapbox/src/main/java/org/odk/collect/mapbox/DynamicPolyLineFeature.kt index 1b9228c27ec..cef92b955ee 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/DynamicPolyLineFeature.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/DynamicPolyLineFeature.kt @@ -22,8 +22,8 @@ internal class DynamicPolyLineFeature( private val featureClickListener: MapFragment.FeatureListener?, private val featureDragEndListener: MapFragment.FeatureListener?, private val lineDescription: LineDescription -) : MapFeature { - val mapPoints = mutableListOf() +) : LineFeature { + override val points = mutableListOf() private val pointAnnotations = mutableListOf() private val pointAnnotationClickListener = ClickListener() private val pointAnnotationDragListener = DragListener() @@ -31,7 +31,7 @@ internal class DynamicPolyLineFeature( init { lineDescription.points.forEach { - mapPoints.add(it) + points.add(it) pointAnnotations.add( MapUtils.createPointAnnotation( pointAnnotationManager, @@ -72,11 +72,11 @@ internal class DynamicPolyLineFeature( } pointAnnotations.clear() - mapPoints.clear() + points.clear() } fun appendPoint(point: MapPoint) { - mapPoints.add(point) + points.add(point) pointAnnotations.add( MapUtils.createPointAnnotation( pointAnnotationManager, @@ -94,13 +94,13 @@ internal class DynamicPolyLineFeature( if (pointAnnotations.isNotEmpty()) { pointAnnotationManager.delete(pointAnnotations.last()) pointAnnotations.removeLast() - mapPoints.removeLast() + points.removeLast() updateLine() } } private fun updateLine() { - val points = mapPoints + val points = points .map { Point.fromLngLat(it.longitude, it.latitude, it.altitude) } @@ -145,7 +145,7 @@ internal class DynamicPolyLineFeature( override fun onAnnotationDrag(annotation: com.mapbox.maps.plugin.annotation.Annotation<*>) { pointAnnotations.forEachIndexed { index, pointAnnotation -> if (annotation.id == pointAnnotation.id) { - mapPoints[index] = MapUtils.mapPointFromPointAnnotation(pointAnnotation) + points[index] = MapUtils.mapPointFromPointAnnotation(pointAnnotation) } } updateLine() diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/LineFeature.kt b/mapbox/src/main/java/org/odk/collect/mapbox/LineFeature.kt new file mode 100644 index 00000000000..130325eb39b --- /dev/null +++ b/mapbox/src/main/java/org/odk/collect/mapbox/LineFeature.kt @@ -0,0 +1,7 @@ +package org.odk.collect.mapbox + +import org.odk.collect.maps.MapPoint + +interface LineFeature : MapFeature { + val points: List +} diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt index c644812a2e7..834918bec40 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt @@ -389,8 +389,8 @@ class MapboxMapFragment : override fun getPolyLinePoints(featureId: Int): List { val feature = features[featureId] - return if (feature is DynamicPolyLineFeature) { - feature.mapPoints + return if (feature is LineFeature) { + feature.points } else { emptyList() } diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolyLineFeature.kt b/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolyLineFeature.kt index 58f7696883d..88ac51e69f8 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolyLineFeature.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolyLineFeature.kt @@ -14,16 +14,16 @@ internal class StaticPolyLineFeature( private val featureId: Int, private val featureClickListener: MapFragment.FeatureListener?, private val lineDescription: LineDescription -) : MapFeature { - private val mapPoints = mutableListOf() +) : LineFeature { + override val points = mutableListOf() private var polylineAnnotation: PolylineAnnotation? = null init { lineDescription.points.forEach { - mapPoints.add(it) + points.add(it) } - val points = mapPoints + val points = points .map { Point.fromLngLat(it.longitude, it.latitude, it.altitude) } @@ -65,6 +65,6 @@ internal class StaticPolyLineFeature( polylineAnnotation?.let { polylineAnnotationManager.delete(it) } - mapPoints.clear() + points.clear() } } From 7abed2cab76c75bf9b0e298e24ff128cb0c223c4 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 18 Jun 2024 14:21:20 +0100 Subject: [PATCH 645/750] Remove @Ignore from test --- .../collect/android/feature/formentry/RequiredQuestionTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/RequiredQuestionTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/RequiredQuestionTest.java index 32c34d68d77..3edc3ad119e 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/RequiredQuestionTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/RequiredQuestionTest.java @@ -6,7 +6,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import org.apache.commons.csv.CSVRecord; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.RuleChain; @@ -101,7 +100,6 @@ public void ifRequiredQuestionIsEmpty_shouldNotBeSavedToAuditLogWhenFormValidate } @Test - @Ignore("https://github.com/getodk/collect/issues/5939") public void ifRequiredQuestionIsInFieldListAndNotFirst_shouldBeValidatedProperly() { rule.startAtMainMenu() .copyForm("requiredQuestionInFieldList.xml") From bdb0d2c547a519e19e561577e612a9e23c5a22bc Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 18 Jun 2024 14:29:06 +0100 Subject: [PATCH 646/750] Replace dangerous matcher in test --- .../feature/formentry/RequiredQuestionTest.java | 2 +- .../android/support/pages/FormEntryPage.java | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/RequiredQuestionTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/RequiredQuestionTest.java index 3edc3ad119e..4c48504fb93 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/RequiredQuestionTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/RequiredQuestionTest.java @@ -104,7 +104,7 @@ public void ifRequiredQuestionIsInFieldListAndNotFirst_shouldBeValidatedProperly rule.startAtMainMenu() .copyForm("requiredQuestionInFieldList.xml") .startBlankForm("requiredQuestionInFieldList") - .answerQuestion(0, "Foo") + .answerQuestion("Foo", true, "blah") .swipeToNextQuestionWithConstraintViolation("Custom required message2") .clickOptionsIcon() .clickOnString(org.odk.collect.strings.R.string.validate) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java index af6150dbd60..4ebd5f1fdc0 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java @@ -301,17 +301,24 @@ public FormEntryPage answerQuestion(String question, String answer) { } public FormEntryPage answerQuestion(String question, boolean isRequired, String answer) { + String questionText; if (isRequired) { - assertQuestionText("* " + question); + questionText = "* " + question; } else { - assertQuestionText(question); + questionText = question; } - inputText(answer); + Matcher classMatcher = withClassName(endsWith("EditText")); + Matcher questionViewMatcher = isQuestionView(questionText); + onView(allOf(classMatcher, isDescendantOfA(questionViewMatcher))).perform(replaceText(answer)); closeSoftKeyboard(); return this; } + /** + * @deprecated Use {@link #answerQuestion(String, String)} instead + */ + @Deprecated public FormEntryPage answerQuestion(int index, String answer) { onView(withIndex(withClassName(endsWith("EditText")), index)).perform(scrollTo()); onView(withIndex(withClassName(endsWith("EditText")), index)).perform(replaceText(answer)); From 6a2ce0f8d6b8e5e0d9156ce823ff4ba54b7ee70d Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 18 Jun 2024 14:35:26 +0100 Subject: [PATCH 647/750] Pull out shared helper for getting question views --- .../odk/collect/android/support/Interactions.kt | 11 +++++++++++ .../android/support/pages/FormEntryPage.java | 17 ++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt index 1f9c209191b..13c2637f304 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt @@ -1,8 +1,10 @@ package org.odk.collect.android.support import android.view.View +import androidx.test.espresso.Espresso.closeSoftKeyboard import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Root +import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.scrollTo import org.hamcrest.Matcher @@ -46,4 +48,13 @@ object Interactions { assertion() } } + + /** + * Replaces text in the view matched by [view] and then closes the keyboard. + */ + @JvmStatic + fun replaceText(view: Matcher, text: String) { + onView(view).perform(ViewActions.replaceText(text)) + closeSoftKeyboard() + } } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java index 4ebd5f1fdc0..77873e366de 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java @@ -23,6 +23,7 @@ import android.os.Build; import android.view.View; +import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import org.hamcrest.Matcher; @@ -308,10 +309,7 @@ public FormEntryPage answerQuestion(String question, boolean isRequired, String questionText = question; } - Matcher classMatcher = withClassName(endsWith("EditText")); - Matcher questionViewMatcher = isQuestionView(questionText); - onView(allOf(classMatcher, isDescendantOfA(questionViewMatcher))).perform(replaceText(answer)); - closeSoftKeyboard(); + Interactions.replaceText(getQuestionFieldMatcher(questionText), answer); return this; } @@ -326,9 +324,7 @@ public FormEntryPage answerQuestion(int index, String answer) { } public FormEntryPage clickOnQuestionField(String questionText) { - Matcher classMatcher = withClassName(endsWith("EditText")); - Matcher questionViewMatcher = isQuestionView(questionText); - Interactions.clickOn(allOf(classMatcher, isDescendantOfA(questionViewMatcher))); + Interactions.clickOn(getQuestionFieldMatcher(questionText)); return this; } @@ -420,6 +416,13 @@ public FormEntryPage assertBackgroundLocationSnackbarShown() { return this; } + private static @NonNull Matcher getQuestionFieldMatcher(String question) { + return allOf( + withClassName(endsWith("EditText")), + isDescendantOfA(isQuestionView(question)) + ); + } + public static class QuestionAndAnswer { private final String question; From a925b68fd688c669c06ec47cf178d66d0b0d42df Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 18 Jun 2024 14:37:11 +0100 Subject: [PATCH 648/750] Make type more specific --- .../org/odk/collect/android/activities/FormFillingActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 39fa6ddd6c1..a358c4453b5 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 @@ -563,7 +563,7 @@ private void setupViewModels(FormEntryViewModelFactory formEntryViewModelFactory formEntryViewModel.getCurrentIndex().observe(this, indexAndValidationResult -> { if (indexAndValidationResult != null) { FormIndex formIndex = indexAndValidationResult.component1(); - ValidationResult validationResult = indexAndValidationResult.component2(); + FailedValidationResult validationResult = indexAndValidationResult.component2(); formIndexAnimationHandler.handle(formIndex); if (validationResult != null) { handleValidationResult(validationResult); From b40c160dd7c9904028103060b7a40ad9a87f267e Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 18 Jun 2024 16:21:10 +0100 Subject: [PATCH 649/750] Fix test question labels --- .../odk/collect/android/feature/formentry/audit/AuditTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/audit/AuditTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/audit/AuditTest.kt index 591a1ce0fd5..b6f385aeb2b 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/audit/AuditTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/audit/AuditTest.kt @@ -67,7 +67,7 @@ class AuditTest { rule.startAtMainMenu() .copyForm("two-question-audit-track-changes.xml") .startBlankForm("One Question Audit Track Changes") - .fillOut(FormEntryPage.QuestionAndAnswer("What is your age", "31")) + .fillOut(FormEntryPage.QuestionAndAnswer("What is your age?", "31")) .clickOptionsIcon() .clickGeneralSettings() @@ -81,7 +81,7 @@ class AuditTest { rule.startAtMainMenu() .copyForm("two-question-audit-track-changes.xml") .startBlankForm("One Question Audit Track Changes") - .fillOut(FormEntryPage.QuestionAndAnswer("What is your age", "31")) + .fillOut(FormEntryPage.QuestionAndAnswer("What is your age?", "31")) .swipeToNextQuestion("What is your name?") .fillOut(FormEntryPage.QuestionAndAnswer("What is your name?", "Adam")) .swipeToEndScreen() From 6f26a9b9b9c78b4c8649903e8642bc1d7c76fccc Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 18 Jun 2024 16:37:09 +0100 Subject: [PATCH 650/750] Rename reference to offline in strings --- strings/src/main/res/values/strings.xml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index fbc1a138feb..95198f604b2 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -479,25 +479,25 @@ Positron Dark Matter - - Reference layer + + Offline layers - + Layer - - Select reference layer + + Select offline layer - - Select the reference layer to use for all maps in this project. You can add options to the list by selecting an MBTile file from your device. + + Select the offline layer to use for all maps in this project. You can add options to the list by selecting an MBTile file from your device. - + Learn more about adding MBTiles. - + Add layers - + Delete layer From ccd0530fa5effe87b86cda1fbbbcf73c17fdc0df Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 18 Jun 2024 16:38:03 +0100 Subject: [PATCH 651/750] Rename string names --- collect_app/src/main/res/xml/maps_preferences.xml | 4 ++-- maps/src/main/res/layout/offline_map_layers_picker.xml | 6 +++--- .../odk/collect/maps/layers/OfflineMapLayersPickerTest.kt | 4 ++-- strings/src/main/res/values-ar/strings.xml | 2 +- strings/src/main/res/values-cs/strings.xml | 2 +- strings/src/main/res/values-da/strings.xml | 2 +- strings/src/main/res/values-de/strings.xml | 2 +- strings/src/main/res/values-es/strings.xml | 2 +- strings/src/main/res/values-fa-rAF/strings.xml | 2 +- strings/src/main/res/values-fa/strings.xml | 2 +- strings/src/main/res/values-fi/strings.xml | 8 ++++---- strings/src/main/res/values-fr/strings.xml | 8 ++++---- strings/src/main/res/values-in/strings.xml | 2 +- strings/src/main/res/values-it/strings.xml | 2 +- strings/src/main/res/values-ja/strings.xml | 2 +- strings/src/main/res/values-pl/strings.xml | 2 +- strings/src/main/res/values-pt/strings.xml | 6 +++--- strings/src/main/res/values-ru/strings.xml | 2 +- strings/src/main/res/values-rw/strings.xml | 2 +- strings/src/main/res/values-sl/strings.xml | 2 +- strings/src/main/res/values-sv-rSE/strings.xml | 2 +- strings/src/main/res/values-sw/strings.xml | 2 +- strings/src/main/res/values-te/strings.xml | 2 +- strings/src/main/res/values-ur/strings.xml | 2 +- strings/src/main/res/values-zh/strings.xml | 2 +- strings/src/main/res/values/strings.xml | 8 ++++---- 26 files changed, 41 insertions(+), 41 deletions(-) diff --git a/collect_app/src/main/res/xml/maps_preferences.xml b/collect_app/src/main/res/xml/maps_preferences.xml index 2b5b76d4c1e..7e4c776fe71 100644 --- a/collect_app/src/main/res/xml/maps_preferences.xml +++ b/collect_app/src/main/res/xml/maps_preferences.xml @@ -9,14 +9,14 @@ app:iconSpaceReserved="false" /> diff --git a/maps/src/main/res/layout/offline_map_layers_picker.xml b/maps/src/main/res/layout/offline_map_layers_picker.xml index 2c407c36d13..27413f894d7 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker.xml @@ -30,7 +30,7 @@ android:id="@+id/title" android:layout_width="0dp" android:layout_height="wrap_content" - android:text="@string/reference_layer_dialog_title" + android:text="@string/offline_layer_dialog_title" android:textAppearance="?textAppearanceTitleLarge" app:layout_constraintStart_toEndOf="@id/guideline_start" app:layout_constraintEnd_toStartOf="@id/guideline_end" @@ -41,7 +41,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/margin_small" - android:text="@string/reference_layer_dialog_description" + android:text="@string/offline_layer_dialog_description" android:textAppearance="?textAppearanceBodyMedium" app:layout_constraintStart_toEndOf="@id/guideline_start" app:layout_constraintEnd_toStartOf="@id/guideline_end" @@ -62,7 +62,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="@dimen/margin_standard" - android:text="@string/get_help_with_reference_layers" + android:text="@string/get_help_with_offline_layers" android:textAppearance="?textAppearanceLabelLarge" android:textColor="?colorAccent" app:layout_constraintBottom_toBottomOf="@id/mbtiles_info_icon" diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index 8704b0b0d44..bef58b15496 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -229,7 +229,7 @@ class OfflineMapLayersPickerTest { fun `the 'learn more' button should be enabled during loading layers`() { launchFragment() - onView(withText(string.get_help_with_reference_layers)).check(matches(isEnabled())) + onView(withText(string.get_help_with_offline_layers)).check(matches(isEnabled())) } @Test @@ -238,7 +238,7 @@ class OfflineMapLayersPickerTest { scheduler.flush() - EspressoHelpers.clickOnText(string.get_help_with_reference_layers) + EspressoHelpers.clickOnText(string.get_help_with_offline_layers) verify(externalWebPageHelper).openWebPageInCustomTab(any(), eq(Uri.parse("https://docs.getodk.org/collect-offline-maps/#transferring-offline-tilesets-to-devices"))) } diff --git a/strings/src/main/res/values-ar/strings.xml b/strings/src/main/res/values-ar/strings.xml index 520a76779b1..7b887314850 100644 --- a/strings/src/main/res/values-ar/strings.xml +++ b/strings/src/main/res/values-ar/strings.xml @@ -381,7 +381,7 @@ بوزيترون المادة المظلمة - الطبقة المرجعية + الطبقة المرجعية diff --git a/strings/src/main/res/values-cs/strings.xml b/strings/src/main/res/values-cs/strings.xml index 9df2d8ad86f..8ab7e0f7a75 100644 --- a/strings/src/main/res/values-cs/strings.xml +++ b/strings/src/main/res/values-cs/strings.xml @@ -386,7 +386,7 @@ Positron Temná hmota - Referenční vrstva + Referenční vrstva diff --git a/strings/src/main/res/values-da/strings.xml b/strings/src/main/res/values-da/strings.xml index 5372b09ce4c..d82eea9261b 100644 --- a/strings/src/main/res/values-da/strings.xml +++ b/strings/src/main/res/values-da/strings.xml @@ -223,7 +223,7 @@ Topografisk Positron - Referencelag + Referencelag diff --git a/strings/src/main/res/values-de/strings.xml b/strings/src/main/res/values-de/strings.xml index f242796b497..57276f51117 100644 --- a/strings/src/main/res/values-de/strings.xml +++ b/strings/src/main/res/values-de/strings.xml @@ -392,7 +392,7 @@ Positron Dunkle Materie - Referenzebene + Referenzebene diff --git a/strings/src/main/res/values-es/strings.xml b/strings/src/main/res/values-es/strings.xml index ade205894a1..1358a425726 100644 --- a/strings/src/main/res/values-es/strings.xml +++ b/strings/src/main/res/values-es/strings.xml @@ -388,7 +388,7 @@ Positrón Materia oscura - Capa de referencia + Capa de referencia diff --git a/strings/src/main/res/values-fa-rAF/strings.xml b/strings/src/main/res/values-fa-rAF/strings.xml index ce2b2f013d6..fd83acbdeed 100644 --- a/strings/src/main/res/values-fa-rAF/strings.xml +++ b/strings/src/main/res/values-fa-rAF/strings.xml @@ -358,7 +358,7 @@ پوزیترون ماده تاریک - مرجع به لایه + مرجع به لایه diff --git a/strings/src/main/res/values-fa/strings.xml b/strings/src/main/res/values-fa/strings.xml index ce2b2f013d6..fd83acbdeed 100644 --- a/strings/src/main/res/values-fa/strings.xml +++ b/strings/src/main/res/values-fa/strings.xml @@ -358,7 +358,7 @@ پوزیترون ماده تاریک - مرجع به لایه + مرجع به لایه diff --git a/strings/src/main/res/values-fi/strings.xml b/strings/src/main/res/values-fi/strings.xml index 4d91b42dc8c..0738b66ab5c 100644 --- a/strings/src/main/res/values-fi/strings.xml +++ b/strings/src/main/res/values-fi/strings.xml @@ -392,15 +392,15 @@ Positroni Pimeä aine - Viitekerros + Viitekerros Kerros - Valitse viitekerros + Valitse viitekerros - Valitse viitekerros jota käytetään kaikissa tämän projektin kartoissa. Voit lisätä vaihtoehtoja listaan valitsemalla MBTile-tiedoston laitteeltasi. + Valitse viitekerros jota käytetään kaikissa tämän projektin kartoissa. Voit lisätä vaihtoehtoja listaan valitsemalla MBTile-tiedoston laitteeltasi. - Opi lisää MBTile-tiedostojen lisäämisestä. + Opi lisää MBTile-tiedostojen lisäämisestä. Lisää kerros diff --git a/strings/src/main/res/values-fr/strings.xml b/strings/src/main/res/values-fr/strings.xml index 04dafa8cd3a..3d84e3b746e 100644 --- a/strings/src/main/res/values-fr/strings.xml +++ b/strings/src/main/res/values-fr/strings.xml @@ -392,15 +392,15 @@ Positron Matière Sombre - Couche de référence + Couche de référence Couche - Sélectionnez la couche de référence + Sélectionnez la couche de référence - Sélectionnez la couche de référence à utiliser pour toutes les cartes de ce projet. Vous pouvez ajouter des options à la liste en sélectionnant un fichier MBTile sur votre appareil. + Sélectionnez la couche de référence à utiliser pour toutes les cartes de ce projet. Vous pouvez ajouter des options à la liste en sélectionnant un fichier MBTile sur votre appareil. - En apprendre plus à propos de l\'ajout de MBTiles. + En apprendre plus à propos de l\'ajout de MBTiles. Ajouter une couche diff --git a/strings/src/main/res/values-in/strings.xml b/strings/src/main/res/values-in/strings.xml index 0b8627c888d..95b9f1e89b5 100644 --- a/strings/src/main/res/values-in/strings.xml +++ b/strings/src/main/res/values-in/strings.xml @@ -385,7 +385,7 @@ Positron Dark Matter - Lapisan referensi + Lapisan referensi diff --git a/strings/src/main/res/values-it/strings.xml b/strings/src/main/res/values-it/strings.xml index 9e30c5fd8e6..6837fae019b 100644 --- a/strings/src/main/res/values-it/strings.xml +++ b/strings/src/main/res/values-it/strings.xml @@ -389,7 +389,7 @@ Posizione Scuro è importante - Livello di riferimento + Livello di riferimento diff --git a/strings/src/main/res/values-ja/strings.xml b/strings/src/main/res/values-ja/strings.xml index 3f33a30eee5..376b92f9133 100644 --- a/strings/src/main/res/values-ja/strings.xml +++ b/strings/src/main/res/values-ja/strings.xml @@ -351,7 +351,7 @@ 位置 ダークマター - 参照レイヤー + 参照レイヤー diff --git a/strings/src/main/res/values-pl/strings.xml b/strings/src/main/res/values-pl/strings.xml index e2f0c257ca0..28bf991572f 100644 --- a/strings/src/main/res/values-pl/strings.xml +++ b/strings/src/main/res/values-pl/strings.xml @@ -331,7 +331,7 @@ Pozyton Ciemna Materia - Warstwa referencyjna + Warstwa referencyjna diff --git a/strings/src/main/res/values-pt/strings.xml b/strings/src/main/res/values-pt/strings.xml index aa214326086..a683232a479 100644 --- a/strings/src/main/res/values-pt/strings.xml +++ b/strings/src/main/res/values-pt/strings.xml @@ -392,14 +392,14 @@ Positron Matéria escura - Camada de referência + Camada de referência Camada - Selecionar camada de referência + Selecionar camada de referência - Saber mais sobre adicionar MBTiles. + Saber mais sobre adicionar MBTiles. Adicionar camada diff --git a/strings/src/main/res/values-ru/strings.xml b/strings/src/main/res/values-ru/strings.xml index de5e650790d..fb7e91ebe50 100644 --- a/strings/src/main/res/values-ru/strings.xml +++ b/strings/src/main/res/values-ru/strings.xml @@ -370,7 +370,7 @@ Топографический Не удалось получить доступ к Google картам. Установлен ли Google Play Services? - Базовый слой + Базовый слой diff --git a/strings/src/main/res/values-rw/strings.xml b/strings/src/main/res/values-rw/strings.xml index eb837dfa80d..b08f0c84591 100644 --- a/strings/src/main/res/values-rw/strings.xml +++ b/strings/src/main/res/values-rw/strings.xml @@ -354,7 +354,7 @@ N\'ukomea guhura n\'iki kibazo, wakigeza kuwa gusabye gukusanya amakuru.Utunyangingo. Ibibazo bidasobanutse cg byijimye. - Indangakitegererezo. + Indangakitegererezo. diff --git a/strings/src/main/res/values-sl/strings.xml b/strings/src/main/res/values-sl/strings.xml index 887e13cf676..5738f864e94 100644 --- a/strings/src/main/res/values-sl/strings.xml +++ b/strings/src/main/res/values-sl/strings.xml @@ -392,7 +392,7 @@ Positron Dark Matter - Referenčni sloj + Referenčni sloj diff --git a/strings/src/main/res/values-sv-rSE/strings.xml b/strings/src/main/res/values-sv-rSE/strings.xml index 14b97dfd22f..100a78d5702 100644 --- a/strings/src/main/res/values-sv-rSE/strings.xml +++ b/strings/src/main/res/values-sv-rSE/strings.xml @@ -310,7 +310,7 @@ Positron Mörk materia - Referenslager + Referenslager diff --git a/strings/src/main/res/values-sw/strings.xml b/strings/src/main/res/values-sw/strings.xml index 8e2755e2d34..1b9334cea2e 100644 --- a/strings/src/main/res/values-sw/strings.xml +++ b/strings/src/main/res/values-sw/strings.xml @@ -344,7 +344,7 @@ Positron Jambo la Giza - Safu ya Kumbukumbu + Safu ya Kumbukumbu diff --git a/strings/src/main/res/values-te/strings.xml b/strings/src/main/res/values-te/strings.xml index e028d66b2ab..b54a5050433 100644 --- a/strings/src/main/res/values-te/strings.xml +++ b/strings/src/main/res/values-te/strings.xml @@ -331,7 +331,7 @@ Positron Dark Matter - రిఫరెన్స్ లేయర్ + రిఫరెన్స్ లేయర్ diff --git a/strings/src/main/res/values-ur/strings.xml b/strings/src/main/res/values-ur/strings.xml index 07e0b2e2f21..8778a8563c8 100644 --- a/strings/src/main/res/values-ur/strings.xml +++ b/strings/src/main/res/values-ur/strings.xml @@ -390,7 +390,7 @@ Positron Dark Matter - ریفرنس Layer + ریفرنس Layer diff --git a/strings/src/main/res/values-zh/strings.xml b/strings/src/main/res/values-zh/strings.xml index a4901ae3f2e..1ada24ddd74 100644 --- a/strings/src/main/res/values-zh/strings.xml +++ b/strings/src/main/res/values-zh/strings.xml @@ -371,7 +371,7 @@ Positron Dark Matter - 应用图层 + 应用图层 diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 95198f604b2..57d22f7b6d4 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -480,19 +480,19 @@ Dark Matter - Offline layers + Offline layers Layer - Select offline layer + Select offline layer - Select the offline layer to use for all maps in this project. You can add options to the list by selecting an MBTile file from your device. + Select the offline layer to use for all maps in this project. You can add options to the list by selecting an MBTile file from your device. - Learn more about adding MBTiles. + Learn more about adding MBTiles. Add layers From 60dffd9fac6212f331e4b95af62a59fa9ee23feb Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 19 Jun 2024 10:33:18 +0100 Subject: [PATCH 652/750] Correct icon used in delete button --- {collect_app => icons}/src/main/res/drawable/ic_delete_24.xml | 0 maps/src/main/res/layout/offline_map_layers_picker_item.xml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {collect_app => icons}/src/main/res/drawable/ic_delete_24.xml (100%) diff --git a/collect_app/src/main/res/drawable/ic_delete_24.xml b/icons/src/main/res/drawable/ic_delete_24.xml similarity index 100% rename from collect_app/src/main/res/drawable/ic_delete_24.xml rename to icons/src/main/res/drawable/ic_delete_24.xml diff --git a/maps/src/main/res/layout/offline_map_layers_picker_item.xml b/maps/src/main/res/layout/offline_map_layers_picker_item.xml index cea34779819..266f0bcd04f 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker_item.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker_item.xml @@ -43,7 +43,7 @@ android:text="@string/delete_layer" android:layout_marginBottom="@dimen/margin_extra_small" android:layout_marginTop="@dimen/margin_extra_small" - app:icon="@drawable/ic_add_white_24" + app:icon="@drawable/ic_delete_24" app:layout_constraintStart_toStartOf="@id/title" app:layout_constraintTop_toBottomOf="@id/path" /> From c2fbd0125661de5d57e8802f0e74710410c518bb Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 19 Jun 2024 13:30:36 +0100 Subject: [PATCH 653/750] Add enum for auto send modes --- .../instancemanagement/autosend/FormExt.kt | 20 +++++++++++++++++-- .../main/java/org/odk/collect/forms/Form.java | 1 + 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt index 5edfdad7bdd..0b5e7709013 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt @@ -10,8 +10,24 @@ fun Form.shouldFormBeSentAutomatically(isAutoSendEnabledInSettings: Boolean): Bo } return if (isAutoSendEnabledInSettings) { - autoSend == null || autoSend.trim().lowercase() != "false" + autoSend == null || getAutoSendMode() != FormAutoSendMode.OPT_OUT } else { - autoSend != null && autoSend.trim().lowercase() == "true" + autoSend != null && getAutoSendMode() == FormAutoSendMode.FORCED } } + +fun Form.getAutoSendMode(): FormAutoSendMode { + return if (autoSend == "false") { + FormAutoSendMode.OPT_OUT + } else if (autoSend?.trim()?.lowercase() == "true") { + FormAutoSendMode.FORCED + } else { + FormAutoSendMode.NEUTRAL + } +} + +enum class FormAutoSendMode { + OPT_OUT, + FORCED, + NEUTRAL +} diff --git a/forms/src/main/java/org/odk/collect/forms/Form.java b/forms/src/main/java/org/odk/collect/forms/Form.java index 62578b725d6..017ad003a42 100644 --- a/forms/src/main/java/org/odk/collect/forms/Form.java +++ b/forms/src/main/java/org/odk/collect/forms/Form.java @@ -262,6 +262,7 @@ public String getLanguage() { return language; } + @Nullable public String getAutoSend() { return autoSend; } From 404a5ef5eca58e4db84359c6b09b19e9dfde8149 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 19 Jun 2024 13:51:08 +0100 Subject: [PATCH 654/750] Use auto send enum everywhere --- .../android/instancemanagement/InstancesDataService.kt | 4 +++- .../android/instancemanagement/autosend/FormExt.kt | 4 ++-- .../autosend/InstanceAutoSendFetcher.kt | 8 ++++---- .../autosend/InstanceAutoSendFetcherTest.kt | 4 +++- 4 files changed, 12 insertions(+), 8 deletions(-) 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 3c5ace9782e..d20610d424b 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 @@ -8,7 +8,9 @@ import org.odk.collect.android.application.Collect import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler import org.odk.collect.android.formentry.FormEntryUseCases import org.odk.collect.android.formmanagement.CollectFormEntryControllerFactory +import org.odk.collect.android.instancemanagement.autosend.FormAutoSendMode import org.odk.collect.android.instancemanagement.autosend.InstanceAutoSendFetcher +import org.odk.collect.android.instancemanagement.autosend.getAutoSendMode import org.odk.collect.android.notifications.Notifier import org.odk.collect.android.openrosa.OpenRosaHttpInterface import org.odk.collect.android.projects.ProjectDependencyProviderFactory @@ -218,7 +220,7 @@ class InstancesDataService( } fun instanceFinalized(projectId: String, form: Form) { - if (form.autoSend != null && form.autoSend == "true") { + if (form.getAutoSendMode() == FormAutoSendMode.FORCED) { instanceSubmitScheduler.scheduleFormAutoSend(projectId) } else { instanceSubmitScheduler.scheduleAutoSend(projectId) diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt index 0b5e7709013..e4f0e5da788 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt @@ -10,9 +10,9 @@ fun Form.shouldFormBeSentAutomatically(isAutoSendEnabledInSettings: Boolean): Bo } return if (isAutoSendEnabledInSettings) { - autoSend == null || getAutoSendMode() != FormAutoSendMode.OPT_OUT + getAutoSendMode() != FormAutoSendMode.OPT_OUT } else { - autoSend != null && getAutoSendMode() == FormAutoSendMode.FORCED + getAutoSendMode() == FormAutoSendMode.FORCED } } 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 25ff079ade1..746d2beb702 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 @@ -10,17 +10,17 @@ object InstanceAutoSendFetcher { fun getInstancesToAutoSend( instancesRepository: InstancesRepository, formsRepository: FormsRepository, - formAutoSend: Boolean = false + forcedOnly: Boolean = false ): List { val allFinalizedForms = instancesRepository.getAllByStatus( Instance.STATUS_COMPLETE, Instance.STATUS_SUBMISSION_FAILED ) - val filter: (Form) -> Boolean = if (formAutoSend) { - { form -> form.autoSend != null && form.autoSend == "true" } + val filter: (Form) -> Boolean = if (forcedOnly) { + { form -> form.getAutoSendMode() == FormAutoSendMode.FORCED } } else { - { form -> form.autoSend == null } + { form -> form.getAutoSendMode() == FormAutoSendMode.NEUTRAL } } return allFinalizedForms.filter { 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 aa0028a1f00..3503d5ef101 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 @@ -85,6 +85,8 @@ class InstanceAutoSendFetcherTest { contains( instanceOfFormWithoutSpecifiedAutoSendComplete.instanceFilePath, instanceOfFormWithoutSpecifiedAutoSendSubmissionFailed.instanceFilePath, + instanceOfFormWithCustomAutoSendComplete.instanceFilePath, + instanceOfFormWithCustomAutoSendSubmissionFailed.instanceFilePath ) ) } @@ -121,7 +123,7 @@ class InstanceAutoSendFetcherTest { val instancesToSend = InstanceAutoSendFetcher.getInstancesToAutoSend( instancesRepository, formsRepository, - formAutoSend = true + forcedOnly = true ) assertThat( From f5e835abc7e951ac0bdc402b2d971a043cfc6f12 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 19 Jun 2024 14:25:19 +0100 Subject: [PATCH 655/750] Make auto-send parsing more lenient --- .../instancemanagement/autosend/FormExt.kt | 2 +- .../autosend/FormExtTest.kt | 64 ++++++++++--------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt index e4f0e5da788..0731c50ecce 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt @@ -17,7 +17,7 @@ fun Form.shouldFormBeSentAutomatically(isAutoSendEnabledInSettings: Boolean): Bo } fun Form.getAutoSendMode(): FormAutoSendMode { - return if (autoSend == "false") { + return if (autoSend?.trim()?.lowercase() == "false") { FormAutoSendMode.OPT_OUT } else if (autoSend?.trim()?.lowercase() == "true") { FormAutoSendMode.FORCED diff --git a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/FormExtTest.kt b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/FormExtTest.kt index e2cc86781ad..c3806db3bc1 100644 --- a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/FormExtTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/FormExtTest.kt @@ -7,72 +7,74 @@ import org.odk.collect.formstest.FormFixtures class FormExtTest { @Test - fun `should return true when auto send is not set on a form level and enabled in settings`() { + fun `#shouldFormBeSentAutomatically returns true when auto send is not set on a form level and enabled in settings`() { val form = FormFixtures.form() val result = form.shouldFormBeSentAutomatically(true) assertThat(result, equalTo(true)) } @Test - fun `should return true when auto send is enabled on a form level and enabled in settings`() { + fun `#shouldFormBeSentAutomatically returns true when auto send is enabled on a form level and enabled in settings`() { val form = FormFixtures.form(autoSend = "true") val result = form.shouldFormBeSentAutomatically(true) assertThat(result, equalTo(true)) } @Test - fun `should return true when auto send is enabled on a form level but not sanitized and enabled in settings`() { - val form = FormFixtures.form(autoSend = " True ") - val result = form.shouldFormBeSentAutomatically(true) - assertThat(result, equalTo(true)) - } - - @Test - fun `should return true when auto send is set on a form level but with a wrong value and enabled in settings`() { - val form = FormFixtures.form(autoSend = "something") - val result = form.shouldFormBeSentAutomatically(true) - assertThat(result, equalTo(true)) - } - - @Test - fun `should return true when auto send is enabled on a form level and disabled in settings`() { + fun `#shouldFormBeSentAutomatically returns true when auto send is enabled on a form level and disabled in settings`() { val form = FormFixtures.form(autoSend = "true") val result = form.shouldFormBeSentAutomatically(false) assertThat(result, equalTo(true)) } @Test - fun `should return true when auto send is enabled on a form level but not sanitized and disabled in settings`() { - val form = FormFixtures.form(autoSend = " True ") - val result = form.shouldFormBeSentAutomatically(false) - assertThat(result, equalTo(true)) - } - - @Test - fun `should return false when auto send is not set on a form level and disabled in settings`() { + fun `#shouldFormBeSentAutomatically returns false when auto send is not set on a form level and disabled in settings`() { val form = FormFixtures.form() val result = form.shouldFormBeSentAutomatically(false) assertThat(result, equalTo(false)) } @Test - fun `should return false when auto send is disabled on a form level and disabled in settings`() { + fun `#shouldFormBeSentAutomatically returns false when auto send is disabled on a form level and disabled in settings`() { val form = FormFixtures.form(autoSend = "false") val result = form.shouldFormBeSentAutomatically(false) assertThat(result, equalTo(false)) } @Test - fun `should return false when auto send is disabled on a form level and enabled in settings`() { + fun `#shouldFormBeSentAutomatically returns false when auto send is disabled on a form level and enabled in settings`() { val form = FormFixtures.form(autoSend = "false") val result = form.shouldFormBeSentAutomatically(true) assertThat(result, equalTo(false)) } @Test - fun `should return false when auto send is set on a form level but with a wrong value and disabled in settings`() { - val form = FormFixtures.form(autoSend = "something") - val result = form.shouldFormBeSentAutomatically(false) - assertThat(result, equalTo(false)) + fun `#getAutoSendMode returns NEUTRAL when autoSend is unsupported`() { + val form = FormFixtures.form(autoSend = "blah") + assertThat(form.getAutoSendMode(), equalTo(FormAutoSendMode.NEUTRAL)) + } + + @Test + fun `#getAutoSendMode returns FORCED when autoSend is true but incorrectly cased`() { + val form = FormFixtures.form(autoSend = "TRUE") + assertThat(form.getAutoSendMode(), equalTo(FormAutoSendMode.FORCED)) + } + + @Test + fun `#getAutoSendMode returns FORCED when autoSend is true but with whitespace`() { + val form = FormFixtures.form(autoSend = " true ") + assertThat(form.getAutoSendMode(), equalTo(FormAutoSendMode.FORCED)) + } + + @Test + fun `#getAutoSendMode returns OPT_OUT when autoSend is false but incorrectly cased`() { + val form = FormFixtures.form(autoSend = "FALSE") + assertThat(form.getAutoSendMode(), equalTo(FormAutoSendMode.OPT_OUT)) + } + + @Test + fun `#getAutoSendMode returns OPT_OUT when autoSend is false but with whitespace`() { + val form = FormFixtures.form(autoSend = " false ") + assertThat(form.getAutoSendMode(), equalTo(FormAutoSendMode.OPT_OUT)) } } From 6f1875e6dd846874fcb67707ff5b9039caff5f96 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 19 Jun 2024 15:00:42 +0100 Subject: [PATCH 656/750] Specify root to make test more stable --- .../java/org/odk/collect/android/support/pages/Page.kt | 7 +++++++ .../android/support/pages/SelectMinimalDialogPage.kt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt index 5869eb1b918..6875b4c9692 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt @@ -189,6 +189,13 @@ abstract class Page> { return this as T } + fun assertTextDoesNotExistInDialog(text: String?): T { + onView(allOf(withText(text), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + .inRoot(isDialog()) + .check(doesNotExist()) + return this as T + } + fun checkIsSnackbarWithQuantityDisplayed(message: Int, quantity: Int): T { return checkIsSnackbarWithMessageDisplayed( ApplicationProvider.getApplicationContext() diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/SelectMinimalDialogPage.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/SelectMinimalDialogPage.kt index 0bfc188f81b..1a853b2385a 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/SelectMinimalDialogPage.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/SelectMinimalDialogPage.kt @@ -2,7 +2,7 @@ package org.odk.collect.android.support.pages class SelectMinimalDialogPage(private val formName: String) : Page() { override fun assertOnPage(): SelectMinimalDialogPage { - assertTextDoesNotExist(formName) + assertTextDoesNotExistInDialog(formName) return this } From 204cf0296654d2618f7e8234d95884f161e4cd6a Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 20 Jun 2024 09:46:22 +0100 Subject: [PATCH 657/750] Make whole importer screen scrollable --- .../layout/offline_map_layers_importer.xml | 287 +++++++++--------- 1 file changed, 151 insertions(+), 136 deletions(-) diff --git a/maps/src/main/res/layout/offline_map_layers_importer.xml b/maps/src/main/res/layout/offline_map_layers_importer.xml index 761f7c3b553..28603285161 100644 --- a/maps/src/main/res/layout/offline_map_layers_importer.xml +++ b/maps/src/main/res/layout/offline_map_layers_importer.xml @@ -19,149 +19,164 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:navigationIcon="@null" - app:title="@string/add_layer"/> + app:title="@string/add_layer" /> - + - + - - - - - - - - - - - - - - - - + android:layout_height="wrap_content"> - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + android:layout_marginBottom="@dimen/margin_extra_extra_small" + android:text="@string/cancel" + app:layout_constraintBottom_toBottomOf="@id/add_layer_button" + app:layout_constraintEnd_toStartOf="@id/add_layer_button" + app:layout_constraintTop_toTopOf="@id/add_layer_button" /> + + + + + + + From 3aed136f7965c1e4eac82f391f521b0a7f875063 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 20 Jun 2024 10:25:22 +0100 Subject: [PATCH 658/750] Add max height to prevent recyclerview from filling the whole screen --- .../main/res/layout/offline_map_layers_importer.xml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/maps/src/main/res/layout/offline_map_layers_importer.xml b/maps/src/main/res/layout/offline_map_layers_importer.xml index 28603285161..d37e995468b 100644 --- a/maps/src/main/res/layout/offline_map_layers_importer.xml +++ b/maps/src/main/res/layout/offline_map_layers_importer.xml @@ -27,6 +27,7 @@ @@ -64,11 +65,10 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/margin_small" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - app:layout_constrainedHeight="true" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHeight_max="320dp" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0" /> + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" /> - From 5bdc01597cb1b1713eaef4517fcc9afeb46cb3ec Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 20 Jun 2024 10:48:28 +0100 Subject: [PATCH 659/750] Don't hold bindings in a field https://developer.android.com/topic/libraries/view-binding#fragments --- .../collect/maps/layers/OfflineMapLayersImporter.kt | 13 ++++++++----- .../collect/maps/layers/OfflineMapLayersPicker.kt | 6 ++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt index a3ca996b4ba..e417aa12e23 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt @@ -21,19 +21,21 @@ class OfflineMapLayersImporter( val viewModel: OfflineMapLayersViewModel by activityViewModels { object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider) as T + return OfflineMapLayersViewModel( + referenceLayerRepository, + scheduler, + settingsProvider + ) as T } } } - private lateinit var binding: OfflineMapLayersImporterBinding - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = OfflineMapLayersImporterBinding.inflate(inflater) + val binding = OfflineMapLayersImporterBinding.inflate(inflater) binding.cancelButton.setOnClickListener { dismiss() @@ -48,6 +50,7 @@ class OfflineMapLayersImporter( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val binding = OfflineMapLayersImporterBinding.bind(view) viewModel.isLoading.observe(this) { isLoading -> if (isLoading) { @@ -74,6 +77,6 @@ class OfflineMapLayersImporter( } override fun getToolbar(): Toolbar { - return binding.toolbar + return OfflineMapLayersImporterBinding.bind(requireView()).toolbar } } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index ce3bcf24d2c..11ade230115 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -48,8 +48,6 @@ class OfflineMapLayersPicker( } } - private lateinit var binding: OfflineMapLayersPickerBinding - private val getLayers = registerForActivityResult(ActivityResultContracts.GetMultipleContents(), registry) { uris -> if (uris.isNotEmpty()) { sharedViewModel.loadLayersToImport(uris, requireContext()) @@ -75,7 +73,7 @@ class OfflineMapLayersPicker( container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = OfflineMapLayersPickerBinding.inflate(inflater) + val binding = OfflineMapLayersPickerBinding.inflate(inflater) binding.mbtilesInfoGroup.addOnClickListener { externalWebPageHelper.openWebPageInCustomTab( @@ -101,7 +99,7 @@ class OfflineMapLayersPicker( } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + val binding = OfflineMapLayersPickerBinding.bind(view) sharedViewModel.isLoading.observe(this) { isLoading -> if (isLoading) { From e3ddf29b86451d555991195b99719618b0ea6c4a Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 20 Jun 2024 11:00:04 +0100 Subject: [PATCH 660/750] Fix tests that need to scroll --- .../odk/collect/maps/layers/OfflineMapLayersImporterTest.kt | 5 +++-- .../odk/collect/maps/layers/OfflineMapLayersPickerTest.kt | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt index a3ae6e91369..83c48cbc80d 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt @@ -4,6 +4,7 @@ import androidx.core.net.toUri import androidx.fragment.app.testing.FragmentScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.scrollTo import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed @@ -203,7 +204,7 @@ class OfflineMapLayersImporterTest { scheduler.flush() - onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) + onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(scrollTo(), click()) scheduler.flush() val fileCaptor = argumentCaptor() @@ -229,7 +230,7 @@ class OfflineMapLayersImporterTest { onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(click()) - onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) + onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(scrollTo(), click()) scheduler.flush() val fileCaptor = argumentCaptor() diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index bef58b15496..ff0db2eebc1 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -350,7 +350,7 @@ class OfflineMapLayersPickerTest { EspressoHelpers.clickOnText(string.add_layer) scheduler.flush() - onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(click()) + onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(scrollTo(), click()) onView(withId(R.id.progress_indicator)).check(matches(isDisplayed())) onView(withId(R.id.layers)).check(matches(not(isDisplayed()))) @@ -375,7 +375,7 @@ class OfflineMapLayersPickerTest { EspressoHelpers.clickOnText(string.add_layer) scheduler.flush() - onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(click()) + onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(scrollTo(), click()) whenever(referenceLayerRepository.getAll()).thenReturn( listOf( ReferenceLayer("1", TempFiles.createTempFile(), file1.name), From d74bf388c5e0647fdcaa3f1230302d3663a98706 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 20 Jun 2024 12:54:31 +0100 Subject: [PATCH 661/750] Move Espresso helpers to test-shared --- .../android/support/pages/ChangesReasonPromptPage.java | 2 +- .../collect/android/support/pages/FillBlankFormPage.java | 2 +- .../odk/collect/android/support/pages/FirstLaunchPage.kt | 2 +- .../odk/collect/android/support/pages/FormEntryPage.java | 4 ++-- .../collect/android/support/pages/FormHierarchyPage.java | 2 +- .../odk/collect/android/support/pages/MainMenuPage.java | 2 +- .../support/pages/ManualProjectCreatorDialogPage.kt | 2 +- .../collect/android/support/pages/NotificationDrawer.kt | 2 +- .../java/org/odk/collect/android/support/pages/Page.kt | 8 ++++---- .../android/support/pages/ProjectSettingsDialogPage.kt | 2 +- .../org/odk/collect/android/support/pages/QRCodePage.java | 2 +- .../main/java/org/odk/collect/testshared}/Interactions.kt | 4 ++-- .../src/main/java/org/odk/collect/testshared}/WaitFor.kt | 2 +- 13 files changed, 18 insertions(+), 18 deletions(-) rename {collect_app/src/androidTest/java/org/odk/collect/android/support => test-shared/src/main/java/org/odk/collect/testshared}/Interactions.kt (94%) rename {collect_app/src/androidTest/java/org/odk/collect/android/support => test-shared/src/main/java/org/odk/collect/testshared}/WaitFor.kt (97%) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ChangesReasonPromptPage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ChangesReasonPromptPage.java index 30a9d388c66..55a46c78f0d 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ChangesReasonPromptPage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ChangesReasonPromptPage.java @@ -9,7 +9,7 @@ import static androidx.test.espresso.matcher.ViewMatchers.withHint; import static androidx.test.espresso.matcher.ViewMatchers.withText; -import org.odk.collect.android.support.WaitFor; +import org.odk.collect.testshared.WaitFor; import java.util.concurrent.Callable; diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FillBlankFormPage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FillBlankFormPage.java index fae7d6d7509..c1168f7915c 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FillBlankFormPage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FillBlankFormPage.java @@ -19,7 +19,7 @@ import androidx.test.espresso.matcher.ViewMatchers; import org.odk.collect.android.R; -import org.odk.collect.android.support.WaitFor; +import org.odk.collect.testshared.WaitFor; public class FillBlankFormPage extends Page { diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt index e05553a6285..33451f2b6c4 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt @@ -1,8 +1,8 @@ package org.odk.collect.android.support.pages import androidx.test.espresso.matcher.ViewMatchers.withSubstring -import org.odk.collect.android.support.Interactions import org.odk.collect.strings.R.string +import org.odk.collect.testshared.Interactions class FirstLaunchPage : Page() { diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java index 77873e366de..254bff24a09 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java @@ -29,8 +29,8 @@ import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.odk.collect.android.R; -import org.odk.collect.android.support.Interactions; -import org.odk.collect.android.support.WaitFor; +import org.odk.collect.testshared.Interactions; +import org.odk.collect.testshared.WaitFor; import java.util.concurrent.Callable; diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormHierarchyPage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormHierarchyPage.java index 5b457e7b4fb..a20c70ddff7 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormHierarchyPage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormHierarchyPage.java @@ -13,7 +13,7 @@ import androidx.test.espresso.contrib.RecyclerViewActions; import org.odk.collect.android.R; -import org.odk.collect.android.support.WaitFor; +import org.odk.collect.testshared.WaitFor; import java.util.concurrent.Callable; 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 aa5c7b9b893..67dc23e4eb3 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 @@ -16,7 +16,7 @@ import org.odk.collect.android.R; import org.odk.collect.android.support.StorageUtils; import org.odk.collect.android.support.TestScheduler; -import org.odk.collect.android.support.WaitFor; +import org.odk.collect.testshared.WaitFor; import org.odk.collect.strings.R.string; import java.io.IOException; diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ManualProjectCreatorDialogPage.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ManualProjectCreatorDialogPage.kt index e8a9530b074..cbd048301de 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ManualProjectCreatorDialogPage.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ManualProjectCreatorDialogPage.kt @@ -3,7 +3,7 @@ package org.odk.collect.android.support.pages import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.matcher.ViewMatchers.withText -import org.odk.collect.android.support.WaitFor.tryAgainOnFail +import org.odk.collect.testshared.WaitFor.tryAgainOnFail class ManualProjectCreatorDialogPage : Page() { override fun assertOnPage(): ManualProjectCreatorDialogPage { diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/NotificationDrawer.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/NotificationDrawer.kt index b0d598f2e7b..e98ac7a7277 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/NotificationDrawer.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/NotificationDrawer.kt @@ -10,7 +10,7 @@ import org.hamcrest.CoreMatchers.`is` import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.nullValue import org.hamcrest.MatcherAssert.assertThat -import org.odk.collect.android.support.WaitFor.waitFor +import org.odk.collect.testshared.WaitFor.waitFor class NotificationDrawer { private var isOpen = false diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt index 6875b4c9692..f0e94e85564 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt @@ -48,10 +48,6 @@ import org.odk.collect.android.R import org.odk.collect.android.application.Collect import org.odk.collect.android.storage.StoragePathProvider import org.odk.collect.android.support.ActivityHelpers.getLaunchIntent -import org.odk.collect.android.support.Interactions -import org.odk.collect.android.support.WaitFor.tryAgainOnFail -import org.odk.collect.android.support.WaitFor.wait250ms -import org.odk.collect.android.support.WaitFor.waitFor import org.odk.collect.android.support.actions.RotateAction import org.odk.collect.android.support.matchers.CustomMatchers.withIndex import org.odk.collect.android.support.rules.RecentAppsRule @@ -61,7 +57,11 @@ import org.odk.collect.androidtest.ActivityScenarioLauncherRule import org.odk.collect.strings.localization.getLocalizedQuantityString import org.odk.collect.strings.localization.getLocalizedString import org.odk.collect.testshared.EspressoHelpers +import org.odk.collect.testshared.Interactions import org.odk.collect.testshared.RecyclerViewMatcher +import org.odk.collect.testshared.WaitFor.tryAgainOnFail +import org.odk.collect.testshared.WaitFor.wait250ms +import org.odk.collect.testshared.WaitFor.waitFor import timber.log.Timber import java.io.File diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ProjectSettingsDialogPage.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ProjectSettingsDialogPage.kt index dc22928343c..f76de62c9db 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ProjectSettingsDialogPage.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ProjectSettingsDialogPage.kt @@ -10,7 +10,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withText import org.hamcrest.Matchers.allOf -import org.odk.collect.android.support.WaitFor +import org.odk.collect.testshared.WaitFor internal class ProjectSettingsDialogPage : Page() { diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/QRCodePage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/QRCodePage.java index 69b9d91d252..7ced75aecb9 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/QRCodePage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/QRCodePage.java @@ -13,7 +13,7 @@ import androidx.test.espresso.Espresso; import org.odk.collect.android.support.ActivityHelpers; -import org.odk.collect.android.support.WaitFor; +import org.odk.collect.testshared.WaitFor; import org.odk.collect.androidtest.DrawableMatcher; public class QRCodePage extends Page { diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt b/test-shared/src/main/java/org/odk/collect/testshared/Interactions.kt similarity index 94% rename from collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt rename to test-shared/src/main/java/org/odk/collect/testshared/Interactions.kt index 13c2637f304..0332079eac2 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt +++ b/test-shared/src/main/java/org/odk/collect/testshared/Interactions.kt @@ -1,4 +1,4 @@ -package org.odk.collect.android.support +package org.odk.collect.testshared import android.view.View import androidx.test.espresso.Espresso.closeSoftKeyboard @@ -8,7 +8,7 @@ import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.scrollTo import org.hamcrest.Matcher -import org.odk.collect.android.support.WaitFor.tryAgainOnFail +import org.odk.collect.testshared.WaitFor.tryAgainOnFail object Interactions { diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/WaitFor.kt b/test-shared/src/main/java/org/odk/collect/testshared/WaitFor.kt similarity index 97% rename from collect_app/src/androidTest/java/org/odk/collect/android/support/WaitFor.kt rename to test-shared/src/main/java/org/odk/collect/testshared/WaitFor.kt index dc0e2f935ef..96c9650ace4 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/WaitFor.kt +++ b/test-shared/src/main/java/org/odk/collect/testshared/WaitFor.kt @@ -1,4 +1,4 @@ -package org.odk.collect.android.support +package org.odk.collect.testshared import junit.framework.AssertionFailedError import java.util.concurrent.Callable From 6c05ed706f18ff7caac73565510258cf56476097 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 20 Jun 2024 13:00:00 +0100 Subject: [PATCH 662/750] Split assertions and interacitons for Espresso helpers --- .../odk/collect/android/support/pages/Page.kt | 81 +++- .../activities/FormFillingActivityTest.kt | 25 +- .../layers/OfflineMapLayersImporterTest.kt | 4 +- .../maps/layers/OfflineMapLayersPickerTest.kt | 412 ++++++++++++++---- .../collect/testshared/AssertIntentsHelper.kt | 2 +- .../{EspressoHelpers.kt => Assertions.kt} | 20 +- 6 files changed, 399 insertions(+), 145 deletions(-) rename test-shared/src/main/java/org/odk/collect/testshared/{EspressoHelpers.kt => Assertions.kt} (70%) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt index f0e94e85564..fa4abd731eb 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt @@ -56,7 +56,7 @@ import org.odk.collect.androidshared.ui.ToastUtils.popRecordedToasts import org.odk.collect.androidtest.ActivityScenarioLauncherRule import org.odk.collect.strings.localization.getLocalizedQuantityString import org.odk.collect.strings.localization.getLocalizedString -import org.odk.collect.testshared.EspressoHelpers +import org.odk.collect.testshared.Assertions import org.odk.collect.testshared.Interactions import org.odk.collect.testshared.RecyclerViewMatcher import org.odk.collect.testshared.WaitFor.tryAgainOnFail @@ -131,7 +131,7 @@ abstract class Page> { } fun assertText(text: String): T { - EspressoHelpers.assertText(text) + Assertions.assertText(text) return this as T } @@ -185,7 +185,12 @@ abstract class Page> { } fun assertTextDoesNotExist(text: String?): T { - onView(allOf(withText(text), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))).check(doesNotExist()) + onView( + allOf( + withText(text), + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) + ) + ).check(doesNotExist()) return this as T } @@ -295,11 +300,13 @@ abstract class Page> { } fun getTranslatedString(id: Int?, vararg formatArgs: Any): String { - return ApplicationProvider.getApplicationContext().getLocalizedString(id!!, *formatArgs) + return ApplicationProvider.getApplicationContext() + .getLocalizedString(id!!, *formatArgs) } fun getTranslatedQuantityString(id: Int?, quantity: Int, vararg formatArgs: Any): String { - return ApplicationProvider.getApplicationContext().getLocalizedQuantityString(id!!, quantity, *formatArgs) + return ApplicationProvider.getApplicationContext() + .getLocalizedQuantityString(id!!, quantity, *formatArgs) } fun clickOnAreaWithIndex(clazz: String?, index: Int): T { @@ -366,25 +373,56 @@ abstract class Page> { } fun checkIsSnackbarErrorVisible(): T { - onView(allOf(withId(com.google.android.material.R.id.snackbar_text))).check(matches(isDisplayed())) + onView(allOf(withId(com.google.android.material.R.id.snackbar_text))).check( + matches( + isDisplayed() + ) + ) return this as T } fun scrollToRecyclerViewItemAndClickText(text: String?): T { - onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(text)), scrollTo())) - onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(text)), click())) + onView(withId(androidx.preference.R.id.recycler_view)).perform( + RecyclerViewActions.actionOnItem( + hasDescendant(withText(text)), + scrollTo() + ) + ) + onView(withId(androidx.preference.R.id.recycler_view)).perform( + RecyclerViewActions.actionOnItem( + hasDescendant(withText(text)), + click() + ) + ) return this as T } fun scrollToRecyclerViewItemAndClickText(string: Int): T { - onView(ViewMatchers.isAssignableFrom(RecyclerView::class.java)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(getTranslatedString(string))), scrollTo())) - onView(ViewMatchers.isAssignableFrom(RecyclerView::class.java)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(getTranslatedString(string))), click())) + onView(ViewMatchers.isAssignableFrom(RecyclerView::class.java)).perform( + RecyclerViewActions.actionOnItem( + hasDescendant(withText(getTranslatedString(string))), + scrollTo() + ) + ) + onView(ViewMatchers.isAssignableFrom(RecyclerView::class.java)).perform( + RecyclerViewActions.actionOnItem( + hasDescendant(withText(getTranslatedString(string))), + click() + ) + ) return this as T } fun clickOnElementInHierarchy(index: Int): T { - onView(withId(R.id.list)).perform(RecyclerViewActions.scrollToPosition(index)) - onView(RecyclerViewMatcher.withRecyclerView(R.id.list).atPositionOnView(index, R.id.primary_text)).perform(click()) + onView(withId(R.id.list)).perform( + RecyclerViewActions.scrollToPosition( + index + ) + ) + onView( + RecyclerViewMatcher.withRecyclerView(R.id.list) + .atPositionOnView(index, R.id.primary_text) + ).perform(click()) return this as T } @@ -394,7 +432,10 @@ abstract class Page> { } fun checkIfElementInHierarchyMatchesToText(text: String?, index: Int): T { - onView(RecyclerViewMatcher.withRecyclerView(R.id.list).atPositionOnView(index, R.id.primary_text)).check(matches(withText(text))) + onView( + RecyclerViewMatcher.withRecyclerView(R.id.list) + .atPositionOnView(index, R.id.primary_text) + ).check(matches(withText(text))) return this as T } @@ -412,7 +453,12 @@ abstract class Page> { } protected fun assertToolbarTitle(title: String?) { - onView(allOf(withText(title), isDescendantOfA(withId(org.odk.collect.androidshared.R.id.toolbar)))).check(matches(isDisplayed())) + onView( + allOf( + withText(title), + isDescendantOfA(withId(org.odk.collect.androidshared.R.id.toolbar)) + ) + ).check(matches(isDisplayed())) } protected fun assertToolbarTitle(title: Int) { @@ -430,11 +476,14 @@ abstract class Page> { } fun clickOnContentDescription(string: Int): T { - EspressoHelpers.clickOnContentDescription(string) + Interactions.clickOn(withContentDescription(string)) return this as T } - fun assertFileWithProjectNameUpdated(sanitizedOldProjectName: String, sanitizedNewProjectName: String): T { + fun assertFileWithProjectNameUpdated( + sanitizedOldProjectName: String, + sanitizedNewProjectName: String + ): T { val storagePathProvider = StoragePathProvider() Assert.assertFalse(File(storagePathProvider.getProjectRootDirPath() + File.separator + sanitizedOldProjectName).exists()) Assert.assertTrue(File(storagePathProvider.getProjectRootDirPath() + File.separator + sanitizedNewProjectName).exists()) diff --git a/collect_app/src/test/java/org/odk/collect/android/activities/FormFillingActivityTest.kt b/collect_app/src/test/java/org/odk/collect/android/activities/FormFillingActivityTest.kt index a3336914708..ed4339c56af 100644 --- a/collect_app/src/test/java/org/odk/collect/android/activities/FormFillingActivityTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/activities/FormFillingActivityTest.kt @@ -7,6 +7,9 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.fragment.app.DialogFragment import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.work.WorkManager import org.hamcrest.MatcherAssert.assertThat @@ -36,12 +39,10 @@ import org.odk.collect.formstest.FormFixtures.form import org.odk.collect.strings.R import org.odk.collect.testshared.ActivityControllerRule import org.odk.collect.testshared.AssertIntentsHelper -import org.odk.collect.testshared.EspressoHelpers.assertText -import org.odk.collect.testshared.EspressoHelpers.assertTextInDialog -import org.odk.collect.testshared.EspressoHelpers.clickOnContentDescription -import org.odk.collect.testshared.EspressoHelpers.clickOnText -import org.odk.collect.testshared.EspressoHelpers.clickOnTextInDialog +import org.odk.collect.testshared.Assertions.assertText +import org.odk.collect.testshared.Assertions.assertTextInDialog import org.odk.collect.testshared.FakeScheduler +import org.odk.collect.testshared.Interactions import org.odk.collect.testshared.RobolectricHelpers.recreateWithProcessRestore import org.robolectric.Shadows.shadowOf import java.io.File @@ -95,7 +96,7 @@ class FormFillingActivityTest { assertText("Two Question") assertText("What is your name?") - clickOnText(R.string.form_forward) + Interactions.clickOn(withText(R.string.form_forward)) scheduler.flush() assertText("What is your age?") @@ -133,11 +134,11 @@ class FormFillingActivityTest { assertText("Two Question") assertText("What is your name?") - clickOnText(R.string.form_forward) + Interactions.clickOn(withText(R.string.form_forward)) scheduler.flush() assertText("What is your age?") - clickOnContentDescription(R.string.view_hierarchy) + Interactions.clickOn(withContentDescription(R.string.view_hierarchy)) assertIntentsHelper.assertNewIntent(FormHierarchyActivity::class) // Recreate and assert we start FormHierarchyActivity @@ -174,7 +175,7 @@ class FormFillingActivityTest { assertText("Two Question") assertText("What is your name?") - clickOnText(R.string.form_forward) + Interactions.clickOn(withText(R.string.form_forward)) scheduler.flush() assertText("What is your age?") @@ -219,12 +220,12 @@ class FormFillingActivityTest { assertText("Two Question") assertText("What is your name?") - clickOnText(R.string.form_forward) + Interactions.clickOn(withText(R.string.form_forward)) scheduler.flush() assertText("What is your age?") // Open external app - clickOnContentDescription(R.string.launch_app) + Interactions.clickOn(withContentDescription(R.string.launch_app)) assertIntentsHelper.assertNewIntent(hasAction("com.example.EXAMPLE")) // Recreate with result @@ -259,7 +260,7 @@ class FormFillingActivityTest { scheduler.flush() assertTextInDialog("This form no longer exists, please email support@getodk.org with a description of what you were doing when this happened.") - clickOnTextInDialog(R.string.ok) + Interactions.clickOn(withText(R.string.ok), root = isDialog()) assertThat(scenario.isFinishing, equalTo(true)) } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt index 83c48cbc80d..d0c2a4bbca6 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt @@ -27,8 +27,8 @@ import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule import org.odk.collect.settings.InMemSettingsProvider import org.odk.collect.shared.TempFiles import org.odk.collect.strings.R -import org.odk.collect.testshared.EspressoHelpers import org.odk.collect.testshared.FakeScheduler +import org.odk.collect.testshared.Interactions import org.odk.collect.testshared.RecyclerViewMatcher import org.odk.collect.testshared.RecyclerViewMatcher.Companion.withRecyclerView import org.odk.collect.testshared.RobolectricHelpers @@ -53,7 +53,7 @@ class OfflineMapLayersImporterTest { launchFragment().onFragment { scheduler.flush() assertThat(it.isVisible, equalTo(true)) - EspressoHelpers.clickOnText(R.string.cancel) + Interactions.clickOn(withText(R.string.cancel)) assertThat(it.isVisible, equalTo(false)) } } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index ff0db2eebc1..9b540cf73db 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -43,8 +43,8 @@ import org.odk.collect.settings.InMemSettingsProvider import org.odk.collect.settings.keys.ProjectKeys import org.odk.collect.shared.TempFiles import org.odk.collect.strings.R.string -import org.odk.collect.testshared.EspressoHelpers import org.odk.collect.testshared.FakeScheduler +import org.odk.collect.testshared.Interactions import org.odk.collect.testshared.RecyclerViewMatcher import org.odk.collect.testshared.RecyclerViewMatcher.Companion.withRecyclerView import org.odk.collect.webpage.ExternalWebPageHelper @@ -64,7 +64,10 @@ class OfflineMapLayersPickerTest { input: I, options: ActivityOptionsCompat? ) { - assertThat(contract, instanceOf(ActivityResultContracts.GetMultipleContents()::class.java)) + assertThat( + contract, + instanceOf(ActivityResultContracts.GetMultipleContents()::class.java) + ) assertThat(input, equalTo("*/*")) dispatchResult(requestCode, uris) } @@ -74,7 +77,13 @@ class OfflineMapLayersPickerTest { val fragmentScenarioLauncherRule = FragmentScenarioLauncherRule( FragmentFactoryBuilder() .forClass(OfflineMapLayersPicker::class) { - OfflineMapLayersPicker(testRegistry, referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) + OfflineMapLayersPicker( + testRegistry, + referenceLayerRepository, + scheduler, + settingsProvider, + externalWebPageHelper + ) }.build() ) @@ -84,7 +93,7 @@ class OfflineMapLayersPickerTest { scenario.onFragment { assertThat(it.isVisible, equalTo(true)) - EspressoHelpers.clickOnText(string.cancel) + Interactions.clickOn(withText(string.cancel)) assertThat(it.isVisible, equalTo(false)) } } @@ -99,8 +108,11 @@ class OfflineMapLayersPickerTest { scheduler.flush() - EspressoHelpers.clickOnText(string.cancel) - assertThat(settingsProvider.getUnprotectedSettings().contains(ProjectKeys.KEY_REFERENCE_LAYER), equalTo(false)) + Interactions.clickOn(withText(string.cancel)) + assertThat( + settingsProvider.getUnprotectedSettings().contains(ProjectKeys.KEY_REFERENCE_LAYER), + equalTo(false) + ) } @Test @@ -118,7 +130,7 @@ class OfflineMapLayersPickerTest { scenario.onFragment { assertThat(it.isVisible, equalTo(true)) - EspressoHelpers.clickOnText(string.save) + Interactions.clickOn(withText(string.save)) assertThat(it.isVisible, equalTo(false)) } } @@ -142,8 +154,11 @@ class OfflineMapLayersPickerTest { scheduler.flush() - EspressoHelpers.clickOnText(string.save) - assertThat(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER), equalTo(null)) + Interactions.clickOn(withText(string.save)) + assertThat( + settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER), + equalTo(null) + ) } @Test @@ -156,9 +171,12 @@ class OfflineMapLayersPickerTest { scheduler.flush() - EspressoHelpers.clickOnText("layer1") - EspressoHelpers.clickOnText(string.save) - assertThat(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER), equalTo("1")) + Interactions.clickOn(withText("layer1")) + Interactions.clickOn(withText(string.save)) + assertThat( + settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER), + equalTo("1") + ) } @Test @@ -171,8 +189,16 @@ class OfflineMapLayersPickerTest { scheduler.flush() - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(isChecked())) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(not(isChecked()))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check( + matches( + isChecked() + ) + ) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check( + matches( + not(isChecked()) + ) + ) } @Test @@ -190,9 +216,21 @@ class OfflineMapLayersPickerTest { scheduler.flush() - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(not(isChecked()))) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(not(isChecked()))) - onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.radio_button)).check(matches(isChecked())) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check( + matches( + not(isChecked()) + ) + ) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check( + matches( + not(isChecked()) + ) + ) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.radio_button)).check( + matches( + isChecked() + ) + ) } @Test @@ -208,8 +246,17 @@ class OfflineMapLayersPickerTest { scheduler.flush() onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(isChecked())) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 0, + R.id.title + ) + ).check(matches(withText(string.none))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check( + matches( + isChecked() + ) + ) } @Test @@ -238,9 +285,12 @@ class OfflineMapLayersPickerTest { scheduler.flush() - EspressoHelpers.clickOnText(string.get_help_with_offline_layers) + Interactions.clickOn(withText(string.get_help_with_offline_layers)) - verify(externalWebPageHelper).openWebPageInCustomTab(any(), eq(Uri.parse("https://docs.getodk.org/collect-offline-maps/#transferring-offline-tilesets-to-devices"))) + verify(externalWebPageHelper).openWebPageInCustomTab( + any(), + eq(Uri.parse("https://docs.getodk.org/collect-offline-maps/#transferring-offline-tilesets-to-devices")) + ) } @Test @@ -250,7 +300,12 @@ class OfflineMapLayersPickerTest { scheduler.flush() onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(1))) - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 0, + R.id.title + ) + ).check(matches(withText(string.none))) } @Test @@ -267,9 +322,24 @@ class OfflineMapLayersPickerTest { scheduler.flush() onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3))) - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.title)).check(matches(withText("layerA"))) - onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.title)).check(matches(withText("layerB"))) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 0, + R.id.title + ) + ).check(matches(withText(string.none))) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 1, + R.id.title + ) + ).check(matches(withText("layerA"))) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 2, + R.id.title + ) + ).check(matches(withText("layerB"))) } @Test @@ -282,13 +352,29 @@ class OfflineMapLayersPickerTest { scheduler.flush() - EspressoHelpers.clickOnText("layer1") - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(not(isChecked()))) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(isChecked())) + Interactions.clickOn(withText("layer1")) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check( + matches( + not(isChecked()) + ) + ) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check( + matches( + isChecked() + ) + ) - EspressoHelpers.clickOnText(string.none) - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(isChecked())) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(not(isChecked()))) + Interactions.clickOn(withText(string.none)) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check( + matches( + isChecked() + ) + ) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check( + matches( + not(isChecked()) + ) + ) } @Test @@ -301,10 +387,18 @@ class OfflineMapLayersPickerTest { scheduler.flush() - EspressoHelpers.clickOnText("layer1") + Interactions.clickOn(withText("layer1")) scenario.recreate() - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(not(isChecked()))) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(isChecked())) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check( + matches( + not(isChecked()) + ) + ) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check( + matches( + isChecked() + ) + ) } @Test @@ -312,7 +406,7 @@ class OfflineMapLayersPickerTest { val scenario = launchFragment() uris.add(Uri.parse("blah")) - EspressoHelpers.clickOnText(string.add_layer) + Interactions.clickOn(withText(string.add_layer)) scenario.onFragment { assertThat( @@ -326,7 +420,7 @@ class OfflineMapLayersPickerTest { fun `clicking the 'add layer' and selecting nothing does not display the confirmation dialog`() { val scenario = launchFragment() - EspressoHelpers.clickOnText(string.add_layer) + Interactions.clickOn(withText(string.add_layer)) scenario.onFragment { assertThat( @@ -348,7 +442,7 @@ class OfflineMapLayersPickerTest { uris.add(file1.toUri()) uris.add(file2.toUri()) - EspressoHelpers.clickOnText(string.add_layer) + Interactions.clickOn(withText(string.add_layer)) scheduler.flush() onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(scrollTo(), click()) @@ -373,7 +467,7 @@ class OfflineMapLayersPickerTest { uris.add(file1.toUri()) uris.add(file2.toUri()) - EspressoHelpers.clickOnText(string.add_layer) + Interactions.clickOn(withText(string.add_layer)) scheduler.flush() onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(scrollTo(), click()) whenever(referenceLayerRepository.getAll()).thenReturn( @@ -385,9 +479,24 @@ class OfflineMapLayersPickerTest { scheduler.flush() onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3))) - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.title)).check(matches(withText(file1.name))) - onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.title)).check(matches(withText(file2.name))) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 0, + R.id.title + ) + ).check(matches(withText(string.none))) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 1, + R.id.title + ) + ).check(matches(withText(file1.name))) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 2, + R.id.title + ) + ).check(matches(withText(file2.name))) } @Test @@ -409,11 +518,13 @@ class OfflineMapLayersPickerTest { @Test fun `recreating maintains expanded layers`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), - ReferenceLayer("2", TempFiles.createTempFile(), "layer2"), - ReferenceLayer("3", TempFiles.createTempFile(), "layer3") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), + ReferenceLayer("2", TempFiles.createTempFile(), "layer2"), + ReferenceLayer("3", TempFiles.createTempFile(), "layer3") + ) + ) val scenario = launchFragment() @@ -434,10 +545,12 @@ class OfflineMapLayersPickerTest { fun `correct path is displayed after expanding layers`() { val file1 = TempFiles.createTempFile() val file2 = TempFiles.createTempFile() - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", file1, "layer1"), - ReferenceLayer("2", file2, "layer2") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", file1, "layer1"), + ReferenceLayer("2", file2, "layer2") + ) + ) launchFragment() @@ -447,22 +560,39 @@ class OfflineMapLayersPickerTest { onView(withId(R.id.layers)).perform(scrollToPosition(2)) onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.arrow)).perform(click()) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.path)).check(matches(withText(file1.absolutePath))) - onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.path)).check(matches(withText(file2.absolutePath))) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.path)).check( + matches( + withText( + file1.absolutePath + ) + ) + ) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.path)).check( + matches( + withText( + file2.absolutePath + ) + ) + ) } @Test fun `clicking delete shows the confirmation dialog`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1") + ) + ) launchFragment() scheduler.flush() onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.arrow)).perform(click()) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform(scrollTo(), click()) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform( + scrollTo(), + click() + ) onView(withText(string.cancel)).inRoot(isDialog()).check(matches(isDisplayed())) onView(withText(string.delete_layer)).inRoot(isDialog()).check(matches(isDisplayed())) @@ -471,23 +601,38 @@ class OfflineMapLayersPickerTest { @Test fun `clicking delete and canceling does not remove the layer`() { val layerFile1 = TempFiles.createTempFile() - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", layerFile1, "layer1") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", layerFile1, "layer1") + ) + ) launchFragment() scheduler.flush() onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.arrow)).perform(click()) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform(scrollTo(), click()) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform( + scrollTo(), + click() + ) onView(withText(string.cancel)).inRoot(isDialog()).perform(click()) onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) onView(withId(R.id.layers)).perform(scrollToPosition(0)) - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.title)).check(matches(withText("layer1"))) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 0, + R.id.title + ) + ).check(matches(withText(string.none))) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 1, + R.id.title + ) + ).check(matches(withText("layer1"))) verify(referenceLayerRepository, never()).delete("1") } @@ -495,24 +640,39 @@ class OfflineMapLayersPickerTest { fun `clicking delete and confirming removes the layer`() { val layerFile1 = TempFiles.createTempFile() val layerFile2 = TempFiles.createTempFile() - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", layerFile1, "layer1"), - ReferenceLayer("2", layerFile2, "layer2") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", layerFile1, "layer1"), + ReferenceLayer("2", layerFile2, "layer2") + ) + ) launchFragment() scheduler.flush() onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.arrow)).perform(click()) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform(scrollTo(), click()) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform( + scrollTo(), + click() + ) onView(withText(string.delete_layer)).inRoot(isDialog()).perform(click()) scheduler.flush() onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.title)).check(matches(withText("layer2"))) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 0, + R.id.title + ) + ).check(matches(withText(string.none))) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 1, + R.id.title + ) + ).check(matches(withText("layer2"))) verify(referenceLayerRepository).delete("1") verify(referenceLayerRepository, never()).delete("2") } @@ -520,9 +680,11 @@ class OfflineMapLayersPickerTest { @Test fun `deleting the selected layer changes selection to 'none' and saves it`() { val layerFile1 = TempFiles.createTempFile() - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", layerFile1, "layer1") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", layerFile1, "layer1") + ) + ) settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, "1") launchFragment() @@ -530,23 +692,40 @@ class OfflineMapLayersPickerTest { scheduler.flush() onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.arrow)).perform(click()) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform(scrollTo(), click()) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform( + scrollTo(), + click() + ) onView(withText(string.delete_layer)).inRoot(isDialog()).perform(click()) scheduler.flush() - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(isChecked())) - assertThat(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER), equalTo(null)) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 0, + R.id.title + ) + ).check(matches(withText(string.none))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check( + matches( + isChecked() + ) + ) + assertThat( + settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER), + equalTo(null) + ) } @Test fun `deleting one of the layers keeps the list sorted in A-Z order`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layerC"), - ReferenceLayer("2", TempFiles.createTempFile(), "layerB"), - ReferenceLayer("3", TempFiles.createTempFile(), "layerA") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layerC"), + ReferenceLayer("2", TempFiles.createTempFile(), "layerB"), + ReferenceLayer("3", TempFiles.createTempFile(), "layerA") + ) + ) launchFragment() @@ -554,30 +733,53 @@ class OfflineMapLayersPickerTest { onView(withId(R.id.layers)).perform(scrollToPosition(2)) onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.arrow)).perform(click()) - onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.delete_layer)).perform(scrollTo(), click()) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.delete_layer)).perform( + scrollTo(), + click() + ) onView(withText(string.delete_layer)).inRoot(isDialog()).perform(click()) scheduler.flush() onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3))) - onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.title)).check(matches(withText(string.none))) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.title)).check(matches(withText("layerA"))) - onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.title)).check(matches(withText("layerC"))) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 0, + R.id.title + ) + ).check(matches(withText(string.none))) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 1, + R.id.title + ) + ).check(matches(withText("layerA"))) + onView( + withRecyclerView(R.id.layers).atPositionOnView( + 2, + R.id.title + ) + ).check(matches(withText("layerC"))) } @Test fun `progress indicator is displayed during deleting layers`() { val layerFile1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", layerFile1, "layer1"), - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", layerFile1, "layer1"), + ) + ) launchFragment() scheduler.flush() onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.arrow)).perform(click()) - onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform(scrollTo(), click()) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.delete_layer)).perform( + scrollTo(), + click() + ) onView(withText(string.delete_layer)).inRoot(isDialog()).perform(click()) onView(withId(R.id.progress_indicator)).check(matches(isDisplayed())) @@ -590,15 +792,35 @@ class OfflineMapLayersPickerTest { } private fun assertLayerCollapsed(position: Int) { - onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.arrow)).check(matches(withImageDrawable(org.odk.collect.icons.R.drawable.ic_baseline_expand_24))) - onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.path)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) - onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.delete_layer)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) + onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.arrow)).check( + matches( + withImageDrawable(org.odk.collect.icons.R.drawable.ic_baseline_expand_24) + ) + ) + onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.path)).check( + matches( + withEffectiveVisibility(ViewMatchers.Visibility.GONE) + ) + ) + onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.delete_layer)).check( + matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)) + ) } private fun assertLayerExpanded(position: Int) { - onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.arrow)).check(matches(withImageDrawable(org.odk.collect.icons.R.drawable.ic_baseline_collapse_24))) - onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.path)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) - onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.delete_layer)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.arrow)).check( + matches( + withImageDrawable(org.odk.collect.icons.R.drawable.ic_baseline_collapse_24) + ) + ) + onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.path)).check( + matches( + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) + ) + ) + onView(withRecyclerView(R.id.layers).atPositionOnView(position, R.id.delete_layer)).check( + matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)) + ) } private fun launchFragment(): FragmentScenario { diff --git a/test-shared/src/main/java/org/odk/collect/testshared/AssertIntentsHelper.kt b/test-shared/src/main/java/org/odk/collect/testshared/AssertIntentsHelper.kt index 69b2043c02d..5fc7a3b88df 100644 --- a/test-shared/src/main/java/org/odk/collect/testshared/AssertIntentsHelper.kt +++ b/test-shared/src/main/java/org/odk/collect/testshared/AssertIntentsHelper.kt @@ -3,7 +3,7 @@ package org.odk.collect.testshared import android.content.Intent import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent import org.hamcrest.Matcher -import org.odk.collect.testshared.EspressoHelpers.assertIntents +import org.odk.collect.testshared.Assertions.assertIntents import kotlin.reflect.KClass class AssertIntentsHelper { diff --git a/test-shared/src/main/java/org/odk/collect/testshared/EspressoHelpers.kt b/test-shared/src/main/java/org/odk/collect/testshared/Assertions.kt similarity index 70% rename from test-shared/src/main/java/org/odk/collect/testshared/EspressoHelpers.kt rename to test-shared/src/main/java/org/odk/collect/testshared/Assertions.kt index ac45a31d9d1..e6298d8632a 100644 --- a/test-shared/src/main/java/org/odk/collect/testshared/EspressoHelpers.kt +++ b/test-shared/src/main/java/org/odk/collect/testshared/Assertions.kt @@ -2,13 +2,11 @@ package org.odk.collect.testshared import android.content.Intent import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.intent.Intents import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE -import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withText import org.hamcrest.CoreMatchers.not @@ -17,7 +15,7 @@ import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.equalTo -object EspressoHelpers { +object Assertions { fun assertText(text: String) { onView(allOf(withText(text), withEffectiveVisibility(VISIBLE))) @@ -30,22 +28,6 @@ object EspressoHelpers { .check(matches(not(doesNotExist()))) } - fun clickOnContentDescription(string: Int) { - onView(withContentDescription(string)).perform(click()) - } - - fun clickOnText(string: Int) { - onView(withText(string)).perform(click()) - } - - fun clickOnText(string: String) { - onView(withText(string)).perform(click()) - } - - fun clickOnTextInDialog(string: Int) { - onView(withText(string)).inRoot(isDialog()).perform(click()) - } - fun assertIntents(vararg matchers: Matcher) { val intents = Intents.getIntents() assertThat(matchers.size, equalTo(intents.size)) From 89a667423191e9add06f77ead29014a84fe8435d Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 20 Jun 2024 13:07:31 +0100 Subject: [PATCH 663/750] Make Assertions and Interactions API more consistent --- .../odk/collect/android/support/pages/Page.kt | 2 +- .../activities/FormFillingActivityTest.kt | 48 ++++++++++--------- .../org/odk/collect/testshared/Assertions.kt | 25 +++++----- 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt index fa4abd731eb..63190d06212 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt @@ -131,7 +131,7 @@ abstract class Page> { } fun assertText(text: String): T { - Assertions.assertText(text) + Assertions.assertText(withText(text)) return this as T } diff --git a/collect_app/src/test/java/org/odk/collect/android/activities/FormFillingActivityTest.kt b/collect_app/src/test/java/org/odk/collect/android/activities/FormFillingActivityTest.kt index ed4339c56af..03ac6ab1f3f 100644 --- a/collect_app/src/test/java/org/odk/collect/android/activities/FormFillingActivityTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/activities/FormFillingActivityTest.kt @@ -40,7 +40,6 @@ import org.odk.collect.strings.R import org.odk.collect.testshared.ActivityControllerRule import org.odk.collect.testshared.AssertIntentsHelper import org.odk.collect.testshared.Assertions.assertText -import org.odk.collect.testshared.Assertions.assertTextInDialog import org.odk.collect.testshared.FakeScheduler import org.odk.collect.testshared.Interactions import org.odk.collect.testshared.RobolectricHelpers.recreateWithProcessRestore @@ -93,12 +92,12 @@ class FormFillingActivityTest { // Start activity val initial = activityControllerRule.build(FormFillingActivity::class.java, intent).setup() scheduler.flush() - assertText("Two Question") - assertText("What is your name?") + assertText(withText("Two Question")) + assertText(withText("What is your name?")) Interactions.clickOn(withText(R.string.form_forward)) scheduler.flush() - assertText("What is your age?") + assertText(withText("What is your age?")) // Recreate and assert we start FormHierarchyActivity val recreated = activityControllerRule.add { @@ -113,8 +112,8 @@ class FormFillingActivityTest { shadowOf(recreated.get()).receiveResult(hierarchyIntent, Activity.RESULT_CANCELED, null) scheduler.flush() - assertText("Two Question") - assertText("What is your age?") + assertText(withText("Two Question")) + assertText(withText("What is your age?")) } @Test @@ -131,12 +130,12 @@ class FormFillingActivityTest { // Start activity val initial = activityControllerRule.build(FormFillingActivity::class.java, intent).setup() scheduler.flush() - assertText("Two Question") - assertText("What is your name?") + assertText(withText("Two Question")) + assertText(withText("What is your name?")) Interactions.clickOn(withText(R.string.form_forward)) scheduler.flush() - assertText("What is your age?") + assertText(withText("What is your age?")) Interactions.clickOn(withContentDescription(R.string.view_hierarchy)) assertIntentsHelper.assertNewIntent(FormHierarchyActivity::class) @@ -154,8 +153,8 @@ class FormFillingActivityTest { shadowOf(recreated.get()).receiveResult(hierarchyIntent, Activity.RESULT_CANCELED, null) scheduler.flush() - assertText("Two Question") - assertText("What is your age?") + assertText(withText("Two Question")) + assertText(withText("What is your age?")) } @Test @@ -172,12 +171,12 @@ class FormFillingActivityTest { // Start activity val initial = activityControllerRule.build(FormFillingActivity::class.java, intent).setup() scheduler.flush() - assertText("Two Question") - assertText("What is your name?") + assertText(withText("Two Question")) + assertText(withText("What is your name?")) Interactions.clickOn(withText(R.string.form_forward)) scheduler.flush() - assertText("What is your age?") + assertText(withText("What is your age?")) val initialFragmentManager = initial.get().supportFragmentManager DialogFragmentUtils.showIfNotShowing(TestDialogFragment::class.java, initialFragmentManager) @@ -199,8 +198,8 @@ class FormFillingActivityTest { shadowOf(recreated.get()).receiveResult(hierarchyIntent, Activity.RESULT_CANCELED, null) scheduler.flush() - assertText("Two Question") - assertText("What is your age?") + assertText(withText("Two Question")) + assertText(withText("What is your age?")) } @Test @@ -217,12 +216,12 @@ class FormFillingActivityTest { // Start activity val initial = activityControllerRule.build(FormFillingActivity::class.java, intent).setup() scheduler.flush() - assertText("Two Question") - assertText("What is your name?") + assertText(withText("Two Question")) + assertText(withText("What is your name?")) Interactions.clickOn(withText(R.string.form_forward)) scheduler.flush() - assertText("What is your age?") + assertText(withText("What is your age?")) // Open external app Interactions.clickOn(withContentDescription(R.string.launch_app)) @@ -237,9 +236,9 @@ class FormFillingActivityTest { scheduler.flush() assertIntentsHelper.assertNoNewIntent() - assertText("Two Question") - assertText("What is your age?") - assertText("159") + assertText(withText("Two Question")) + assertText(withText("What is your age?")) + assertText(withText("159")) } /** @@ -258,7 +257,10 @@ class FormFillingActivityTest { val scenario = scenarioLauncherRule.launch(intent) scheduler.flush() - assertTextInDialog("This form no longer exists, please email support@getodk.org with a description of what you were doing when this happened.") + assertText( + withText("This form no longer exists, please email support@getodk.org with a description of what you were doing when this happened."), + root = isDialog() + ) Interactions.clickOn(withText(R.string.ok), root = isDialog()) assertThat(scenario.isFinishing, equalTo(true)) diff --git a/test-shared/src/main/java/org/odk/collect/testshared/Assertions.kt b/test-shared/src/main/java/org/odk/collect/testshared/Assertions.kt index e6298d8632a..6730512d7ab 100644 --- a/test-shared/src/main/java/org/odk/collect/testshared/Assertions.kt +++ b/test-shared/src/main/java/org/odk/collect/testshared/Assertions.kt @@ -1,14 +1,14 @@ package org.odk.collect.testshared import android.content.Intent +import android.view.View import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Root import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.intent.Intents -import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility -import androidx.test.espresso.matcher.ViewMatchers.withText import org.hamcrest.CoreMatchers.not import org.hamcrest.Matcher import org.hamcrest.MatcherAssert.assertThat @@ -17,22 +17,21 @@ import org.hamcrest.Matchers.equalTo object Assertions { - fun assertText(text: String) { - onView(allOf(withText(text), withEffectiveVisibility(VISIBLE))) - .check(matches(not(doesNotExist()))) - } + fun assertText(view: Matcher, root: Matcher? = null) { + val onView = if (root != null) { + onView(allOf(view, withEffectiveVisibility(VISIBLE))).inRoot(root) + } else { + onView(allOf(view, withEffectiveVisibility(VISIBLE))) + } - fun assertTextInDialog(text: String) { - onView(allOf(withText(text), withEffectiveVisibility(VISIBLE))) - .inRoot(isDialog()) - .check(matches(not(doesNotExist()))) + onView.check(matches(not(doesNotExist()))) } - fun assertIntents(vararg matchers: Matcher) { + fun assertIntents(vararg intentMatchers: Matcher) { val intents = Intents.getIntents() - assertThat(matchers.size, equalTo(intents.size)) + assertThat(intentMatchers.size, equalTo(intents.size)) - matchers.forEachIndexed { index, matcher -> + intentMatchers.forEachIndexed { index, matcher -> assertThat(intents[index], matcher) } } From 9bf9e60b1c8d68b84f24aaa57bddb4ad907963f5 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 20 Jun 2024 13:38:35 +0100 Subject: [PATCH 664/750] Use scroll safe helpers --- .../maps/layers/OfflineMapLayersImporterTest.kt | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt index d0c2a4bbca6..f3f0cbb5ccd 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt @@ -3,8 +3,6 @@ package org.odk.collect.maps.layers import androidx.core.net.toUri import androidx.fragment.app.testing.FragmentScenario import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.scrollTo import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed @@ -64,7 +62,7 @@ class OfflineMapLayersImporterTest { scheduler.flush() assertThat(it.isVisible, equalTo(true)) it.viewModel.loadLayersToImport(emptyList(), it.requireContext()) - onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) + Interactions.clickOn(withId(org.odk.collect.maps.R.id.add_layer_button)) scheduler.flush() RobolectricHelpers.runLooper() assertThat(it.isVisible, equalTo(false)) @@ -120,12 +118,12 @@ class OfflineMapLayersImporterTest { launchFragment() scheduler.flush() - onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(click()) + Interactions.clickOn(withId(org.odk.collect.maps.R.id.current_project_option)) onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(not(isChecked()))) onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(isChecked())) - onView(withId(org.odk.collect.maps.R.id.all_projects_option)).perform(click()) + Interactions.clickOn(withId(org.odk.collect.maps.R.id.all_projects_option)) onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(isChecked())) onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(not(isChecked()))) @@ -136,7 +134,7 @@ class OfflineMapLayersImporterTest { val scenario = launchFragment() scheduler.flush() - onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(click()) + Interactions.clickOn(withId(org.odk.collect.maps.R.id.current_project_option)) scenario.recreate() @@ -204,7 +202,7 @@ class OfflineMapLayersImporterTest { scheduler.flush() - onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(scrollTo(), click()) + Interactions.clickOn(withId(org.odk.collect.maps.R.id.add_layer_button)) scheduler.flush() val fileCaptor = argumentCaptor() @@ -228,9 +226,8 @@ class OfflineMapLayersImporterTest { scheduler.flush() - onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(click()) - - onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(scrollTo(), click()) + Interactions.clickOn(withId(org.odk.collect.maps.R.id.current_project_option)) + Interactions.clickOn(withId(org.odk.collect.maps.R.id.add_layer_button)) scheduler.flush() val fileCaptor = argumentCaptor() From 1c555e0d07480e88bf531caf86ea58c3e9bd5404 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 20 Jun 2024 14:04:56 +0100 Subject: [PATCH 665/750] Remove tests already covered by parent implementation tests --- .../android/widgets/DecimalWidgetTest.java | 34 ++++++----------- .../android/widgets/ExDecimalWidgetTest.java | 36 ++++++------------ .../android/widgets/ExIntegerWidgetTest.java | 33 +++++----------- .../android/widgets/IntegerWidgetTest.java | 29 ++++---------- .../widgets/StringNumberWidgetTest.java | 38 +++++-------------- 5 files changed, 51 insertions(+), 119 deletions(-) diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/DecimalWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/DecimalWidgetTest.java index baeed76c95a..ea6bdc64f26 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/DecimalWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/DecimalWidgetTest.java @@ -1,5 +1,16 @@ package org.odk.collect.android.widgets; +import static junit.framework.TestCase.assertEquals; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; +import static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP; + +import android.text.InputType; +import android.text.method.SingleLineTransformationMethod; + import androidx.annotation.NonNull; import org.javarosa.core.model.Constants; @@ -9,27 +20,12 @@ import org.junit.Test; import org.mockito.Mock; import org.odk.collect.android.formentry.questions.QuestionDetails; -import org.odk.collect.android.utilities.Appearances; import org.odk.collect.android.widgets.base.GeneralStringWidgetTest; import java.text.NumberFormat; import java.util.Locale; import java.util.Random; -import static junit.framework.TestCase.assertEquals; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.when; -import static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP; - -import android.text.InputType; -import android.text.method.PasswordTransformationMethod; -import android.text.method.SingleLineTransformationMethod; - public class DecimalWidgetTest extends GeneralStringWidgetTest { private final Random random = new Random(); @@ -222,12 +218,4 @@ public void verifyInputType() { assertThat(widget.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL)); assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(SingleLineTransformationMethod.class)); } - - @Test - public void answersShouldNotBeMaskedIfMaskedAppearanceIsUsed() { - when(formEntryPrompt.getAppearanceHint()).thenReturn(Appearances.MASKED); - - assertThat(getSpyWidget().widgetAnswerText.getBinding().editText.getTransformationMethod(), is(not(instanceOf(PasswordTransformationMethod.class)))); - assertThat(getSpyWidget().widgetAnswerText.getBinding().textView.getTransformationMethod(), is(not(instanceOf(PasswordTransformationMethod.class)))); - } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/ExDecimalWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/ExDecimalWidgetTest.java index 89d281291d6..52cb2f71780 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/ExDecimalWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/ExDecimalWidgetTest.java @@ -1,14 +1,24 @@ package org.odk.collect.android.widgets; +import static junit.framework.TestCase.assertEquals; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; +import static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP; + +import android.text.InputType; +import android.text.method.SingleLineTransformationMethod; + import androidx.annotation.NonNull; import org.javarosa.core.model.Constants; import org.javarosa.core.model.data.DecimalData; -import org.odk.collect.android.formentry.questions.QuestionDetails; import org.javarosa.core.model.data.IAnswerData; import org.junit.Test; import org.mockito.Mock; -import org.odk.collect.android.utilities.Appearances; +import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.widgets.base.GeneralExStringWidgetTest; import org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry; import org.odk.collect.android.widgets.utilities.StringRequester; @@ -16,20 +26,6 @@ import java.text.NumberFormat; import java.util.Locale; -import static junit.framework.TestCase.assertEquals; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.when; -import static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP; - -import android.text.InputType; -import android.text.method.PasswordTransformationMethod; -import android.text.method.SingleLineTransformationMethod; - /** * @author James Knight */ @@ -106,12 +102,4 @@ public void verifyInputType() { assertThat(widget.binding.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(SingleLineTransformationMethod.class)); assertThat(widget.binding.widgetAnswerText.getBinding().textView.getTransformationMethod(), equalTo(null)); } - - @Test - public void answersShouldNotBeMaskedIfMaskedAppearanceIsUsed() { - when(formEntryPrompt.getAppearanceHint()).thenReturn(Appearances.MASKED); - - assertThat(getSpyWidget().binding.widgetAnswerText.getBinding().editText.getTransformationMethod(), is(not(instanceOf(PasswordTransformationMethod.class)))); - assertThat(getSpyWidget().binding.widgetAnswerText.getBinding().textView.getTransformationMethod(), is(not(instanceOf(PasswordTransformationMethod.class)))); - } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/ExIntegerWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/ExIntegerWidgetTest.java index ba99b0013ec..546bd1136b2 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/ExIntegerWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/ExIntegerWidgetTest.java @@ -1,30 +1,25 @@ package org.odk.collect.android.widgets; +import static junit.framework.TestCase.assertEquals; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.when; +import static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP; + +import android.text.InputType; +import android.text.method.SingleLineTransformationMethod; + import androidx.annotation.NonNull; import org.javarosa.core.model.Constants; import org.javarosa.core.model.data.IntegerData; +import org.junit.Test; import org.mockito.Mock; import org.odk.collect.android.formentry.questions.QuestionDetails; -import org.junit.Test; -import org.odk.collect.android.utilities.Appearances; import org.odk.collect.android.widgets.base.GeneralExStringWidgetTest; import org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry; import org.odk.collect.android.widgets.utilities.StringRequester; -import static junit.framework.TestCase.assertEquals; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.mockito.Mockito.when; -import static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP; - -import android.text.InputType; -import android.text.method.PasswordTransformationMethod; -import android.text.method.SingleLineTransformationMethod; - /** * @author James Knight */ @@ -81,12 +76,4 @@ public void verifyInputType() { assertThat(widget.binding.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(SingleLineTransformationMethod.class)); assertThat(widget.binding.widgetAnswerText.getBinding().textView.getTransformationMethod(), equalTo(null)); } - - @Test - public void answersShouldNotBeMaskedIfMaskedAppearanceIsUsed() { - when(formEntryPrompt.getAppearanceHint()).thenReturn(Appearances.MASKED); - - assertThat(getSpyWidget().binding.widgetAnswerText.getBinding().editText.getTransformationMethod(), is(not(instanceOf(PasswordTransformationMethod.class)))); - assertThat(getSpyWidget().binding.widgetAnswerText.getBinding().textView.getTransformationMethod(), is(not(instanceOf(PasswordTransformationMethod.class)))); - } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/IntegerWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/IntegerWidgetTest.java index 8fec8b44717..37282e96c4d 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/IntegerWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/IntegerWidgetTest.java @@ -1,27 +1,22 @@ package org.odk.collect.android.widgets; -import androidx.annotation.NonNull; - -import org.javarosa.core.model.Constants; -import org.javarosa.core.model.data.IntegerData; -import org.odk.collect.android.formentry.questions.QuestionDetails; -import org.junit.Test; -import org.odk.collect.android.utilities.Appearances; -import org.odk.collect.android.widgets.base.GeneralStringWidgetTest; - import static junit.framework.TestCase.assertEquals; import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.core.Is.is; import static org.mockito.Mockito.when; import static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP; import android.text.InputType; -import android.text.method.PasswordTransformationMethod; import android.text.method.SingleLineTransformationMethod; +import androidx.annotation.NonNull; + +import org.javarosa.core.model.Constants; +import org.javarosa.core.model.data.IntegerData; +import org.junit.Test; +import org.odk.collect.android.formentry.questions.QuestionDetails; +import org.odk.collect.android.widgets.base.GeneralStringWidgetTest; + /** * @author James Knight */ @@ -67,12 +62,4 @@ public void verifyInputType() { assertThat(widget.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED)); assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(SingleLineTransformationMethod.class)); } - - @Test - public void answersShouldNotBeMaskedIfMaskedAppearanceIsUsed() { - when(formEntryPrompt.getAppearanceHint()).thenReturn(Appearances.MASKED); - - assertThat(getSpyWidget().widgetAnswerText.getBinding().editText.getTransformationMethod(), is(not(instanceOf(PasswordTransformationMethod.class)))); - assertThat(getSpyWidget().widgetAnswerText.getBinding().textView.getTransformationMethod(), is(not(instanceOf(PasswordTransformationMethod.class)))); - } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/StringNumberWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/StringNumberWidgetTest.java index ea7b26b4a37..caf9500376d 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/StringNumberWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/StringNumberWidgetTest.java @@ -1,20 +1,6 @@ package org.odk.collect.android.widgets; -import androidx.annotation.NonNull; - -import net.bytebuddy.utility.RandomString; - -import org.javarosa.core.model.Constants; -import org.javarosa.core.model.data.StringData; -import org.odk.collect.android.formentry.questions.QuestionDetails; -import org.junit.Test; -import org.odk.collect.android.utilities.Appearances; -import org.odk.collect.android.widgets.base.GeneralStringWidgetTest; - import static junit.framework.TestCase.assertEquals; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Mockito.when; @@ -25,6 +11,16 @@ import android.text.method.PasswordTransformationMethod; import android.text.method.SingleLineTransformationMethod; +import androidx.annotation.NonNull; + +import net.bytebuddy.utility.RandomString; + +import org.javarosa.core.model.Constants; +import org.javarosa.core.model.data.StringData; +import org.junit.Test; +import org.odk.collect.android.formentry.questions.QuestionDetails; +import org.odk.collect.android.widgets.base.GeneralStringWidgetTest; + /** * @author James Knight */ @@ -74,18 +70,4 @@ public void verifyInputTypeWithMaskedAppearance() { assertThat(widget.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_NUMBER)); assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(PasswordTransformationMethod.class)); } - - @Test - public void answersShouldNotBeMaskedIfMaskedAppearanceIsNotUsed() { - assertThat(getSpyWidget().widgetAnswerText.getBinding().editText.getTransformationMethod(), is(not(instanceOf(PasswordTransformationMethod.class)))); - assertThat(getSpyWidget().widgetAnswerText.getBinding().textView.getTransformationMethod(), is(not(instanceOf(PasswordTransformationMethod.class)))); - } - - @Test - public void answersShouldBeMaskedIfMaskedAppearanceIsUsed() { - when(formEntryPrompt.getAppearanceHint()).thenReturn(Appearances.MASKED); - - assertThat(getSpyWidget().widgetAnswerText.getBinding().editText.getTransformationMethod(), is(instanceOf(PasswordTransformationMethod.class))); - assertThat(getSpyWidget().widgetAnswerText.getBinding().textView.getTransformationMethod(), is(instanceOf(PasswordTransformationMethod.class))); - } } From 686ab8f15f0475712befe9f98d5aef37f9b41316 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 20 Jun 2024 14:23:43 +0100 Subject: [PATCH 666/750] Don't allow masked appearance for string number widget --- .../java/org/odk/collect/android/utilities/Appearances.kt | 5 +++-- .../org/odk/collect/android/utilities/AppearancesTest.kt | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/utilities/Appearances.kt b/collect_app/src/main/java/org/odk/collect/android/utilities/Appearances.kt index 78d4067709f..12f3331f62e 100644 --- a/collect_app/src/main/java/org/odk/collect/android/utilities/Appearances.kt +++ b/collect_app/src/main/java/org/odk/collect/android/utilities/Appearances.kt @@ -20,7 +20,6 @@ import org.javarosa.core.model.Constants import org.javarosa.form.api.FormEntryPrompt import org.odk.collect.android.dynamicpreload.ExternalDataUtil import org.odk.collect.androidshared.utils.ScreenUtils -import java.lang.Exception object Appearances { // Date appearances @@ -196,6 +195,8 @@ object Appearances { @JvmStatic fun isMasked(prompt: FormEntryPrompt): Boolean { val appearance = getSanitizedAppearanceHint(prompt) - return appearance.contains(MASKED) && prompt.dataType == Constants.DATATYPE_TEXT + return appearance.contains(MASKED) && + !appearance.contains(NUMBERS) && + prompt.dataType == Constants.DATATYPE_TEXT } } diff --git a/collect_app/src/test/java/org/odk/collect/android/utilities/AppearancesTest.kt b/collect_app/src/test/java/org/odk/collect/android/utilities/AppearancesTest.kt index aa718aa2363..017504a632c 100644 --- a/collect_app/src/test/java/org/odk/collect/android/utilities/AppearancesTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/utilities/AppearancesTest.kt @@ -401,4 +401,11 @@ class AppearancesTest { whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_DECIMAL) assertFalse(Appearances.isMasked(formEntryPrompt)) } + + @Test + fun `isMasked returns false when 'masked' appearance is found for text questions with 'numbers' appearance`() { + whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_TEXT) + whenever(formEntryPrompt.appearanceHint).thenReturn("masked numbers") + assertFalse(Appearances.isMasked(formEntryPrompt)) + } } From d0434eb291b131196471267e8c7d4d95c9b2a65a Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 20 Jun 2024 16:17:53 +0100 Subject: [PATCH 667/750] Update STATE.md --- docs/STATE.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/STATE.md b/docs/STATE.md index a75cfa86152..91a837fb62d 100644 --- a/docs/STATE.md +++ b/docs/STATE.md @@ -1,8 +1,6 @@ # State of the union -The purpose of this document is to give anyone who reads it a quick overview -of both the current state and the direction of the code. The community should try -and update this document as the code evolves. +The purpose of this document is to give anyone who reads it a quick overview of both the current state and the direction of the code. The community should try and update this document as the code evolves. ## How we got here @@ -21,27 +19,29 @@ and update this document as the code evolves. * UI is "iconic" (old) but with a lot of inconsistencies and quirks and is best adapted to small screens (although often used on tablets) * A lot of code lives in between one "god" Activity (`FormFillingActivity`) and a process singleton (`FormController`) * Core form entry flow uses custom side-to-side swipe view (in `FormFillingActivity` made up of `ODKView`) -* Questions are rendered using a view "framework" of implementations inheriting from `QuestionWidget` (which is documented at in [WIDGETS.MD](WIDGETS.md)) +* Questions are rendered using a view "framework" of implementations inheriting from `QuestionWidget` (which is documented at in [WIDGETS.MD](WIDGETS.md)) which are also used to store UI state during form entry * App mostly stores data in flat files indexed in SQLite * Access to data in SQLite happens through repository objects which deal in data/domain objects (`FormsRepository` and `Form` for example) * Settings UIs for the app use Android's Preferences abstraction -* App uses [Material 3 Theming](https://m3.material.io/foundations/customization) so [Material components](https://material.io/components?platform=android) (using [Material 3 components](https://m3.material.io/) styles) are preferred over custom or platform ones. +* App uses [Material 3 Theming](https://m3.material.io/foundations/customization) so [Material components](https://material.io/components?platform=android) are preferred over custom or platform ones. * Dagger2 is used to inject "black box" objects such as Activity and just uses a very basic setup * Http is handled using OkHttp3 and https client abstractions are generally wrapped in Android's AsyncTask -* Geo activities use three engines (Mapbox, osmdroid, Google Maps) depending on the selected basemap even though Mapbox could do everything osmdroid does +* Geo activities use three engines (Mapbox, osmdroid, Google Maps) depending on the selected basemap * Code goes through static analysis using CheckStyle, PMD, ktlint and Android Lint * Forms get into the app from two different sources (Open Rosa servers and disk) but the logic for this is disparate and they don't sit behind a common interface * Instances are linked to the forms they are instances of through formid and version. However, the same formid and version combination could represent multiple forms in storage * `SharedPreferences` is wrapped in app's own `Settings` abstraction +* The form hierarchy is rendered using `FormHierarchyActivity` which hasn't been seriously touched (at a code or design) level for a few years ## Where we're going -* General effort to increase test coverage and quality while working on anything and pushing more for tests in PR review -* Slowly moving responsibilities out of `FormFillingActivity` -* Writing pretty much all new code in Kotlin +* General effort to increase test coverage and quality while working on anything and enforcing tests for new code in PR review +* Moving responsibilities out of `FormFillingActivity` +* Writing all new code in Kotlin * Writing new code using a [multi-module approach](CODE-GUIDELINES.md#gradle-sub-modules) (feature modules, mini frameworks etc) and breaking old code out into modules when opportunities come up * Trying to remove technical debt flagged with `@Deprecated` -* Replacing async work such as `AsyncTask` with `LiveData` + `Scheduler` abstraction +* Replacing async work such as `AsyncTask` with `Flow`/`LiveData` + `Scheduler` abstraction * Gradually removing use of `CursorLoader` (all remaining uses are in `CursorLoaderFactory`) * Using AndroidX Test in new local tests and migrating other local tests as we touch them (from classic Robolectric) -* Moving towards a ["data services"](data_services_architecture.pdf) oriented architecture that has emerged over time +* Moving towards a ["data services"](data_services_architecture.pdf) oriented architecture that has emerged over time that uses AndroidX Architecture Components for the core of the UI (Fragment, View, ViewModel etc) +* Improving the `MapFragment` abstraction so more logic can be shared between the map engines From 2af2ea3ca83250bb6587575e7c2b96c1cc9a68e5 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 21 Jun 2024 12:52:46 +0100 Subject: [PATCH 668/750] Remove video resolution analytics --- .../collect/android/analytics/AnalyticsEvents.kt | 13 ------------- .../odk/collect/android/widgets/VideoWidget.java | 7 +------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index 703eff2ac40..21915ec3b3d 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -7,19 +7,6 @@ object AnalyticsEvents { */ const val SET_SERVER = "SetServer" - /** - * Track video requests with high resolution setting turned off. The action should be a hash of - * the form definition. - */ - const val REQUEST_VIDEO_NOT_HIGH_RES = "RequestVideoNotHighRes" - - /** - * Track video requests with high resolution setting turned on. This is tracked to contextualize - * the counts with the high resolution setting turned off since we expect that video is not very - * common overall. The action should be a hash of the form definition. - */ - const val REQUEST_HIGH_RES_VIDEO = "RequestHighResVideo" - /** * Track submission encryption. The action should be a hash of the form definition. */ diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/VideoWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/VideoWidget.java index eb553578119..eca5256f0e8 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/VideoWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/VideoWidget.java @@ -14,8 +14,6 @@ package org.odk.collect.android.widgets; -import static org.odk.collect.android.analytics.AnalyticsEvents.REQUEST_HIGH_RES_VIDEO; -import static org.odk.collect.android.analytics.AnalyticsEvents.REQUEST_VIDEO_NOT_HIGH_RES; import static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes; import android.annotation.SuppressLint; @@ -32,7 +30,6 @@ import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.data.StringData; import org.javarosa.form.api.FormEntryPrompt; -import org.odk.collect.analytics.Analytics; import org.odk.collect.android.databinding.VideoWidgetBinding; import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.utilities.Appearances; @@ -163,10 +160,8 @@ private void captureVideo() { boolean highResolution = settingsProvider.getUnprotectedSettings().getBoolean(ProjectKeys.KEY_HIGH_RESOLUTION); if (highResolution) { i.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); - Analytics.log(REQUEST_HIGH_RES_VIDEO, "form"); - } else { - Analytics.log(REQUEST_VIDEO_NOT_HIGH_RES, "form"); } + try { waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex()); ((Activity) getContext()).startActivityForResult(i, requestCode); From 2a78019b2d8d91dbe303df0f2eab5554c3bf0b58 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 21 Jun 2024 12:59:46 +0100 Subject: [PATCH 669/750] Remove OSM analytics event --- .../odk/collect/android/activities/FormFillingActivity.java | 3 --- .../org/odk/collect/android/analytics/AnalyticsEvents.kt | 5 ----- 2 files changed, 8 deletions(-) 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 a358c4453b5..a4852cb6740 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 @@ -18,7 +18,6 @@ import static android.content.DialogInterface.BUTTON_POSITIVE; import static android.view.animation.AnimationUtils.loadAnimation; import static org.javarosa.form.api.FormEntryController.EVENT_PROMPT_NEW_REPEAT; -import static org.odk.collect.android.analytics.AnalyticsEvents.OPEN_MAP_KIT_RESPONSE; import static org.odk.collect.android.formentry.FormIndexAnimationHandler.Direction.BACKWARDS; import static org.odk.collect.android.formentry.FormIndexAnimationHandler.Direction.FORWARDS; import static org.odk.collect.android.utilities.AnimationUtils.areAnimationsEnabled; @@ -80,7 +79,6 @@ import org.jetbrains.annotations.NotNull; import org.joda.time.DateTime; import org.joda.time.LocalDateTime; -import org.odk.collect.analytics.Analytics; import org.odk.collect.android.R; import org.odk.collect.android.analytics.AnalyticsUtils; import org.odk.collect.android.application.Collect; @@ -865,7 +863,6 @@ protected void onActivityResult(int requestCode, int resultCode, final Intent in switch (requestCode) { case RequestCodes.OSM_CAPTURE: - Analytics.log(OPEN_MAP_KIT_RESPONSE, "form"); setWidgetData(intent.getStringExtra("OSM_FILE_NAME")); break; case RequestCodes.EX_ARBITRARY_FILE_CHOOSER: diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index 21915ec3b3d..2cb1f91fd4b 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -30,11 +30,6 @@ object AnalyticsEvents { */ const val IMPORT_AND_ENCRYPT_INSTANCE = "ImportAndEncryptInstance" - /** - * Tracks responses from OpenMapKit to the OSMWidget - */ - const val OPEN_MAP_KIT_RESPONSE = "OpenMapKitResponse" - /** * Tracks how often users create shortcuts to forms */ From e4dfecaeb3421426637e59bdaa460f4762aca1e3 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 21 Jun 2024 13:03:13 +0100 Subject: [PATCH 670/750] Remove shortcuts analytics event --- .../org/odk/collect/android/analytics/AnalyticsEvents.kt | 5 ----- .../collect/android/external/AndroidShortcutsActivity.kt | 6 ------ 2 files changed, 11 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index 2cb1f91fd4b..030342a43d0 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -30,11 +30,6 @@ object AnalyticsEvents { */ const val IMPORT_AND_ENCRYPT_INSTANCE = "ImportAndEncryptInstance" - /** - * Tracks how often users create shortcuts to forms - */ - const val CREATE_SHORTCUT = "CreateShortcut" - /** * Tracks how often instances that have been deleted on disk are opened for editing/viewing */ diff --git a/collect_app/src/main/java/org/odk/collect/android/external/AndroidShortcutsActivity.kt b/collect_app/src/main/java/org/odk/collect/android/external/AndroidShortcutsActivity.kt index fe2d5394beb..e0c5d059422 100644 --- a/collect_app/src/main/java/org/odk/collect/android/external/AndroidShortcutsActivity.kt +++ b/collect_app/src/main/java/org/odk/collect/android/external/AndroidShortcutsActivity.kt @@ -21,8 +21,6 @@ import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.odk.collect.android.R -import org.odk.collect.android.analytics.AnalyticsEvents -import org.odk.collect.android.analytics.AnalyticsUtils import org.odk.collect.android.formlists.blankformlist.BlankFormListItem import org.odk.collect.android.formlists.blankformlist.BlankFormListViewModel import org.odk.collect.android.injection.DaggerUtils @@ -59,10 +57,6 @@ class AndroidShortcutsActivity : AppCompatActivity() { .map { it.formName } .toTypedArray() ) { _: DialogInterface?, item: Int -> - AnalyticsUtils.logServerEvent( - AnalyticsEvents.CREATE_SHORTCUT, - settingsProvider.getUnprotectedSettings() - ) val intent = getShortcutIntent(blankFormListItems, item) setResult(RESULT_OK, intent) finish() From b214c177b132229c6e3b3a96f6371c5079abdf95 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 21 Jun 2024 13:22:34 +0100 Subject: [PATCH 671/750] Remove import events --- .../android/analytics/AnalyticsEvents.kt | 12 ------------ .../InstanceDiskSynchronizer.java | 17 ++--------------- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index 030342a43d0..6711a1c323c 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -18,18 +18,6 @@ object AnalyticsEvents { */ const val SUBMISSION = "Submission" - /** - * Tracks if any forms are being used as part of a workflow where instances are imported - * from disk - */ - const val IMPORT_INSTANCE = "ImportInstance" - - /** - * Tracks if any forms are being used as part of a workflow where instances are imported - * from disk and then encrypted - */ - const val IMPORT_AND_ENCRYPT_INSTANCE = "ImportAndEncryptInstance" - /** * Tracks how often instances that have been deleted on disk are opened for editing/viewing */ diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceDiskSynchronizer.java b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceDiskSynchronizer.java index 79b06569cfc..ea8dd695898 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceDiskSynchronizer.java +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceDiskSynchronizer.java @@ -14,11 +14,11 @@ package org.odk.collect.android.instancemanagement; +import static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString; + import android.net.Uri; import org.apache.commons.io.FileUtils; -import org.odk.collect.android.analytics.AnalyticsEvents; -import org.odk.collect.android.analytics.AnalyticsUtils; import org.odk.collect.android.application.Collect; import org.odk.collect.android.exception.EncryptionException; import org.odk.collect.android.external.InstancesContract; @@ -50,8 +50,6 @@ import timber.log.Timber; -import static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString; - public class InstanceDiskSynchronizer { private static int counter; @@ -187,22 +185,11 @@ private String getInstanceIdFromInstance(final String instancePath) { private void encryptInstanceIfNeeded(Form form, Instance instance) throws EncryptionException, IOException { if (instance != null) { if (shouldInstanceBeEncrypted(form)) { - logImportAndEncrypt(form); encryptInstance(instance); - } else { - logImport(form); } } } - private void logImport(Form form) { - AnalyticsUtils.logFormEvent(AnalyticsEvents.IMPORT_INSTANCE, form.getFormId(), form.getDisplayName()); - } - - private void logImportAndEncrypt(Form form) { - AnalyticsUtils.logFormEvent(AnalyticsEvents.IMPORT_AND_ENCRYPT_INSTANCE, form.getFormId(), form.getDisplayName()); - } - private void encryptInstance(Instance instance) throws EncryptionException, IOException { String instancePath = instance.getInstanceFilePath(); File instanceXml = new File(instancePath); From 7cb509090c1188735ed0706b6a649dd354be906f Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 21 Jun 2024 13:28:16 +0100 Subject: [PATCH 672/750] Remove partial form submission event --- .../org/odk/collect/android/analytics/AnalyticsEvents.kt | 5 ----- .../java/org/odk/collect/android/tasks/SaveFormToDisk.java | 5 ----- 2 files changed, 10 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index 6711a1c323c..30664536840 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -99,11 +99,6 @@ object AnalyticsEvents { */ const val FORM_LEVEL_AUTO_SEND = "FormLevelAutoSend" - /** - * Tracks how often a form is finalized using a `ref` attribute on the `submission` element - */ - const val PARTIAL_FORM_FINALIZED = "PartialFormFinalized" - /** * Tracks how often drafts that can't be bulk finalized are attempted to be */ diff --git a/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java b/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java index 600a2679198..c40fd464a0a 100644 --- a/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java +++ b/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java @@ -39,7 +39,6 @@ import org.json.JSONException; import org.json.JSONObject; import org.odk.collect.analytics.Analytics; -import org.odk.collect.android.analytics.AnalyticsEvents; import org.odk.collect.android.application.Collect; import org.odk.collect.android.database.instances.DatabaseInstanceColumns; import org.odk.collect.android.exception.EncryptionException; @@ -357,10 +356,6 @@ private Instance exportData(boolean markCompleted, FormSaver.ProgressListener pr // now see if the packaging of the data for the server would make it // non-reopenable (e.g., encryption or other fraction of the form). boolean canEditAfterCompleted = formController.isSubmissionEntireForm(); - if (!canEditAfterCompleted) { - Analytics.log(AnalyticsEvents.PARTIAL_FORM_FINALIZED, "form"); - } - boolean isEncrypted = false; // build a submission.xml to hold the data being submitted From 25668f8bafe979661f14ee13a0ed1b6c3d4f6c57 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 21 Jun 2024 13:32:03 +0100 Subject: [PATCH 673/750] Remove form level auto send event --- .../org/odk/collect/android/analytics/AnalyticsEvents.kt | 5 ----- .../collect/android/instancemanagement/autosend/FormExt.kt | 6 ------ 2 files changed, 11 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index 30664536840..6b36d52650d 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -94,11 +94,6 @@ object AnalyticsEvents { */ const val FORM_LEVEL_AUTO_DELETE = "FormLevelAutoDelete" - /** - * Tracks how often form-level auto-send setting is used - */ - const val FORM_LEVEL_AUTO_SEND = "FormLevelAutoSend" - /** * Tracks how often drafts that can't be bulk finalized are attempted to be */ diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt index 0731c50ecce..8b88a59c852 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt @@ -1,14 +1,8 @@ package org.odk.collect.android.instancemanagement.autosend -import org.odk.collect.android.analytics.AnalyticsEvents -import org.odk.collect.android.analytics.AnalyticsUtils import org.odk.collect.forms.Form fun Form.shouldFormBeSentAutomatically(isAutoSendEnabledInSettings: Boolean): Boolean { - if (!autoSend.isNullOrEmpty()) { - AnalyticsUtils.logFormEvent(AnalyticsEvents.FORM_LEVEL_AUTO_SEND, formId, displayName) - } - return if (isAutoSendEnabledInSettings) { getAutoSendMode() != FormAutoSendMode.OPT_OUT } else { From 12bedc48ae3ded534e5bd014cbd547d83f7ddfe9 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 21 Jun 2024 13:35:33 +0100 Subject: [PATCH 674/750] Remove auto delete analytics event --- .../org/odk/collect/android/analytics/AnalyticsEvents.kt | 5 ----- .../collect/android/utilities/InstanceAutoDeleteChecker.kt | 6 ------ 2 files changed, 11 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index 6b36d52650d..770ef2d647d 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -89,11 +89,6 @@ object AnalyticsEvents { const val INSTANCE_PROVIDER_DELETE = "InstanceProviderDelete" - /** - * Tracks how often form-level auto-delete setting is used - */ - const val FORM_LEVEL_AUTO_DELETE = "FormLevelAutoDelete" - /** * Tracks how often drafts that can't be bulk finalized are attempted to be */ diff --git a/collect_app/src/main/java/org/odk/collect/android/utilities/InstanceAutoDeleteChecker.kt b/collect_app/src/main/java/org/odk/collect/android/utilities/InstanceAutoDeleteChecker.kt index 0b05398d560..2202958c87b 100644 --- a/collect_app/src/main/java/org/odk/collect/android/utilities/InstanceAutoDeleteChecker.kt +++ b/collect_app/src/main/java/org/odk/collect/android/utilities/InstanceAutoDeleteChecker.kt @@ -1,7 +1,5 @@ package org.odk.collect.android.utilities -import org.odk.collect.android.analytics.AnalyticsEvents -import org.odk.collect.android.analytics.AnalyticsUtils import org.odk.collect.forms.FormsRepository import org.odk.collect.forms.instances.Instance import java.util.Locale @@ -21,10 +19,6 @@ object InstanceAutoDeleteChecker { instance: Instance ): Boolean { formsRepository.getLatestByFormIdAndVersion(instance.formId, instance.formVersion)?.let { form -> - if (!form.autoDelete.isNullOrEmpty()) { - AnalyticsUtils.logFormEvent(AnalyticsEvents.FORM_LEVEL_AUTO_DELETE, form.formId, form.displayName) - } - return if (isAutoDeleteEnabledInProjectSettings) { form.autoDelete == null || form.autoDelete.trim().lowercase(Locale.US) != "false" } else { From 34ba35198817e9c2063199d61c896f094efed393 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 21 Jun 2024 13:37:35 +0100 Subject: [PATCH 675/750] Remove ExPrinterWidget event --- .../org/odk/collect/android/analytics/AnalyticsEvents.kt | 5 ----- .../org/odk/collect/android/widgets/ExPrinterWidget.java | 5 +---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index 770ef2d647d..1d4cc4da641 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -95,11 +95,6 @@ object AnalyticsEvents { const val BULK_FINALIZE_ENCRYPTED_FORM = "BulkFinalizeEncryptedForm" const val BULK_FINALIZE_SAVE_POINT = "BulkFinalizeSavePoint" - /** - * Tracks how often printing with the old ExPrinterWidget is triggered - */ - const val ZEBRA_PRINTER_STARTED = "ZebraPrinterStarted" - /** * Tracks how often saved forms are manually deleted and in what number */ diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/ExPrinterWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/ExPrinterWidget.java index 4d777e4724d..af2c2cadb2a 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/ExPrinterWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/ExPrinterWidget.java @@ -27,8 +27,6 @@ import androidx.annotation.NonNull; import org.javarosa.core.model.data.IAnswerData; -import org.odk.collect.analytics.Analytics; -import org.odk.collect.android.analytics.AnalyticsEvents; import org.javarosa.form.api.FormEntryPrompt; import org.odk.collect.android.databinding.ExPrinterWidgetBinding; import org.odk.collect.android.formentry.questions.QuestionDetails; @@ -187,7 +185,6 @@ private void onButtonClick() { try { waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex()); firePrintingActivity(intentName); - Analytics.log(AnalyticsEvents.ZEBRA_PRINTER_STARTED, "form"); } catch (ActivityNotFoundException e) { waitingForDataRegistry.cancelWaitingForData(); Toast.makeText(getContext(), @@ -240,4 +237,4 @@ private void firePrintingActivity(String intentName) throws ActivityNotFoundExce bcastIntent.putExtra("DATA", printDataBundle); getContext().sendBroadcast(bcastIntent); } -} \ No newline at end of file +} From 27bf5f4a01b293a05555dc305d9eeb71d4fa90ea Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 21 Jun 2024 13:53:55 +0100 Subject: [PATCH 676/750] Add events to track layers import usage --- .../android/analytics/AnalyticsEvents.kt | 6 +++--- maps/build.gradle.kts | 1 + .../org/odk/collect/maps/AnalyticsEvents.kt | 11 +++++++++++ .../maps/layers/OfflineMapLayersViewModel.kt | 18 +++++++++++++++++- 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 maps/src/main/java/org/odk/collect/maps/AnalyticsEvents.kt diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index 1d4cc4da641..e103f7b8432 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -98,7 +98,7 @@ object AnalyticsEvents { /** * Tracks how often saved forms are manually deleted and in what number */ - const val DELETE_SAVED_FORM_FEW = "DeleteSavedFormFew" - const val DELETE_SAVED_FORM_TENS = "DeleteSavedFormTens" - const val DELETE_SAVED_FORM_HUNDREDS = "DeleteSavedFormHundreds" + const val DELETE_SAVED_FORM_FEW = "DeleteSavedFormFew" // < 10 + const val DELETE_SAVED_FORM_TENS = "DeleteSavedFormTens" // >= 10 + const val DELETE_SAVED_FORM_HUNDREDS = "DeleteSavedFormHundreds" // >= 100 } diff --git a/maps/build.gradle.kts b/maps/build.gradle.kts index 627fa4906f7..81b6d7ab8b1 100644 --- a/maps/build.gradle.kts +++ b/maps/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(project(":settings")) implementation(project(":strings")) implementation(project(":web-page")) + implementation(project(":analytics")) implementation(Dependencies.android_material) implementation(Dependencies.kotlin_stdlib) implementation(Dependencies.androidx_fragment_ktx) diff --git a/maps/src/main/java/org/odk/collect/maps/AnalyticsEvents.kt b/maps/src/main/java/org/odk/collect/maps/AnalyticsEvents.kt new file mode 100644 index 00000000000..cfcf7bfed56 --- /dev/null +++ b/maps/src/main/java/org/odk/collect/maps/AnalyticsEvents.kt @@ -0,0 +1,11 @@ +package org.odk.collect.maps + +object AnalyticsEvents { + + /** + * Tracks how many offline layers people are importing at once + */ + const val IMPORT_LAYER_SINGLE = "ImportLayerSingle" // One + const val IMPORT_LAYER_FEW = "ImportLayerFew" // <= 5 + const val IMPORT_LAYER_MANY = "ImportLayerMany" // > 5 +} diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index 1674d8f503e..1b507b51df5 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -5,10 +5,12 @@ import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import org.odk.collect.analytics.Analytics import org.odk.collect.androidshared.system.copyToFile import org.odk.collect.androidshared.system.getFileExtension import org.odk.collect.androidshared.system.getFileName import org.odk.collect.async.Scheduler +import org.odk.collect.maps.AnalyticsEvents import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.keys.ProjectKeys import org.odk.collect.shared.TempFiles @@ -75,7 +77,10 @@ class OfflineMapLayersViewModel( _isLoading.value = true scheduler.immediate( background = { - tempLayersDir.listFiles()?.forEach { + val layers = tempLayersDir.listFiles() + logImport(layers) + + layers?.forEach { referenceLayerRepository.addLayer(it, shared) } tempLayersDir.delete() @@ -99,4 +104,15 @@ class OfflineMapLayersViewModel( _isLoading.postValue(false) } } + + private fun logImport(layers: Array?) { + val count = layers?.size ?: return + val event = when { + count == 1 -> AnalyticsEvents.IMPORT_LAYER_SINGLE + count <= 5 -> AnalyticsEvents.IMPORT_LAYER_FEW + else -> AnalyticsEvents.IMPORT_LAYER_MANY + } + + Analytics.log(event) + } } From 811ea0629ec42950ccb1966aa9e84a363ec672e6 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 21 Jun 2024 14:01:25 +0100 Subject: [PATCH 677/750] Add analytics doc to track answered questions --- docs/ANALYTICS-QUESTIONS.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/ANALYTICS-QUESTIONS.md diff --git a/docs/ANALYTICS-QUESTIONS.md b/docs/ANALYTICS-QUESTIONS.md new file mode 100644 index 00000000000..00a44b5fcd7 --- /dev/null +++ b/docs/ANALYTICS-QUESTIONS.md @@ -0,0 +1,15 @@ +# Analytics questions + +A list of questions asked and answered via analytics already sectioned by the date (with a 90 day recording window). + +## June 2024 + +- How often is high-res video disabled? 2% of users using video widget. +- How often is `OSMWidget` used? Once. +- How many users create shortcuts? 126. +- How many user are using ADB to add forms/instances? 904 for Forms, 2728 for instances (500k events). +- How many users are importing encrypted instances? 10. +- How often is partial form finalization used? Never. +- How many forms use auto send attribute? 200+ (max recordable) with 1.5k users. +- How many forms use auto delete attribute? 200+ (max recordable) with 1k users. +- How often is old printer widget (`ExPrinterWidget`) used? Never. From 64e09609e79dfc8a4650f522660bbe8cef7a3016 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 21 Jun 2024 15:33:33 +0100 Subject: [PATCH 678/750] Make sure delete is mentioned in offline maps blurb --- strings/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 57d22f7b6d4..16e44c88931 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -489,7 +489,7 @@ Select offline layer - Select the offline layer to use for all maps in this project. You can add options to the list by selecting an MBTile file from your device. + Select the offline layer to use for all maps in this project. You can add options to the list by selecting an MBTile file from your device and also delete existing layers from the list. Learn more about adding MBTiles. From fa6c7c48a06468afb48ad81195b47438dc97f725 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 21 Jun 2024 16:16:37 +0100 Subject: [PATCH 679/750] Rework release planning details --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e046d26e106..853274b9e9c 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,14 @@ Please note that the `master` branch reflects ongoing development and is not pro ## Release cycle -Releases are planned to happen every 2-3 months (resulting in ~4 releases a year). Soon before (or just after) the end of one release cycle, the core team will plan a new set of work for the next release based on the [ODK Roadmap](https://getodk.org/roadmap), bugs and crashes identified in previous releases and other required or preemptive maintenance. This work will be broken down into Github Issues (for things that aren't already) by [@seadowg](https://github.com/seadowg) and is then added into Collect's prioritised [backlog](https://github.com/orgs/getodk/projects/9/views/8) for the core team (and any external contributors) to work on day to day. Sometimes issues will be assigned to core team members before they are actually started (moved to "in progress") to make it clear who's going to be working on what. +Releases are planned to happen every 2-3 months (resulting in ~4 releases a year). Soon before (or just after) the end of one release cycle, the core team will plan a new set of work for the next release. This involves: + +1. Moving issues not finished in the last release and new items from the [ODK Roadmap](https://getodk.org/roadmap) to the [planning board](https://github.com/orgs/getodk/projects/9/views/25) +2. Giving the core team a few days to review and reflect on the planning board +3. The core team will then meet to trim work not that will not be included in the next release and pitch alternative things to work on +4. The milestone for the new release is added to [the backlog](https://github.com/orgs/getodk/projects/9) and is prioritized + +Sometimes issues will be assigned to core team members before they are actually started (moved to "in progress") to make it clear who's going to be working on what. Once the majority of high risk or visible work is done for a release, a new beta will then be released to the Play Store by [@lognaturel](https://github.com/lognaturel) and that will be used for regression testing by [@getodk/testers](https://github.com/orgs/getodk/teams/testers). If any problems are found, the release is blocked until we can merge fixes. Regression testing should continue on the original beta build (rather than a new one with fixes) unless problems block the rest of testing. Once the process is complete, [@lognaturel](https://github.com/lognaturel) pushes the releases to the Play Store following [these instructions](#creating-signed-releases-for-google-play-store). From 38639e6210eb57890b3d426c00df492705d9c201 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 21 Jun 2024 18:00:12 +0100 Subject: [PATCH 680/750] Fix file type name --- strings/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 16e44c88931..1bbeef9ba40 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -489,7 +489,7 @@ Select offline layer - Select the offline layer to use for all maps in this project. You can add options to the list by selecting an MBTile file from your device and also delete existing layers from the list. + Select the offline layer to use for all maps in this project. You can add options to the list by selecting an MBTiles file from your device and also delete existing layers from the list. Learn more about adding MBTiles. From 7887637c5a24a8e92ab903ec811c02b39178888f Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 24 Jun 2024 11:49:43 +0200 Subject: [PATCH 681/750] Added a failing test --- .../DirectoryReferenceLayerRepositoryTest.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index fc0275c7e96..b6958454935 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -16,7 +16,7 @@ import java.io.File class DirectoryReferenceLayerRepositoryTest { private val sharedLayersDir = TempFiles.createTempDir() private val projectLayersDir = TempFiles.createTempDir() - private val mapConfigurator = StubMapConfigurator() + private var mapConfigurator = StubMapConfigurator() private val repository = DirectoryReferenceLayerRepository( sharedLayersDir.absolutePath, projectLayersDir.absolutePath, @@ -206,6 +206,23 @@ class DirectoryReferenceLayerRepositoryTest { assertThat(repository.getAll(), contains(fileLayer2)) } + @Test // https://github.com/getodk/collect/issues/6211 + fun mapConfiguratorThatRepresentsTheCurrentConfigurationShouldBeUsedEveryTimeTheRepositoryIsCalled() { + val file = TempFiles.createTempFile(sharedLayersDir) + + mapConfigurator.apply { + addFile(file, false, file.name) + } + + assertThat(repository.getAll().isEmpty(), equalTo(true)) + + mapConfigurator = StubMapConfigurator().apply { + addFile(file, true, file.name) + } + + assertThat(repository.getAll().isEmpty(), equalTo(false)) + } + private class StubMapConfigurator : MapConfigurator { private val files = mutableMapOf>() From bdbef33d6cbc243c14dba2f4c5e8382d1189178c Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 24 Jun 2024 12:02:37 +0200 Subject: [PATCH 682/750] Always use map configurator that represents the current state --- .../android/injection/config/AppDependencyModule.java | 4 +++- .../maps/layers/DirectoryReferenceLayerRepository.kt | 8 ++++---- .../maps/layers/DirectoryReferenceLayerRepositoryTest.kt | 5 ++--- .../maps/layers/MapFragmentReferenceLayerUtilsTest.kt | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) 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 7fa9eaf2a97..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 @@ -574,7 +574,9 @@ public ReferenceLayerRepository providesReferenceLayerRepository(StoragePathProv return new DirectoryReferenceLayerRepository( storagePathProvider.getOdkDirPath(StorageSubdirectory.SHARED_LAYERS), storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS), - MapConfiguratorProvider.getConfigurator(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_BASEMAP_SOURCE)) + () -> MapConfiguratorProvider.getConfigurator( + settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_BASEMAP_SOURCE) + ) ); } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt index 06c3cf8e57d..0c848ed4210 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt @@ -8,20 +8,20 @@ import java.io.File class DirectoryReferenceLayerRepository( private val sharedLayersDirPath: String, private val projectLayersDirPath: String, - private val mapConfigurator: MapConfigurator + private val getMapConfigurator: () -> MapConfigurator ) : ReferenceLayerRepository { override fun getAll(): List { return getAllFilesWithDirectory() .map { ReferenceLayer(getIdForFile(it.second, it.first), it.first, getName(it.first)) } .distinctBy { it.id } - .filter { mapConfigurator.supportsLayer(it.file) } + .filter { getMapConfigurator().supportsLayer(it.file) } } override fun get(id: String): ReferenceLayer? { val file = getAllFilesWithDirectory().firstOrNull { getIdForFile(it.second, it.first) == id } - return if (file != null && mapConfigurator.supportsLayer(file.first)) { + return if (file != null && getMapConfigurator().supportsLayer(file.first)) { ReferenceLayer(getIdForFile(file.second, file.first), file.first, getName(file.first)) } else { null @@ -50,6 +50,6 @@ class DirectoryReferenceLayerRepository( PathUtils.getRelativeFilePath(directoryPath, file.absolutePath) private fun getName(file: File): String { - return mapConfigurator.getDisplayName(file) + return getMapConfigurator().getDisplayName(file) } } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index b6958454935..9891c729fb4 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -19,9 +19,8 @@ class DirectoryReferenceLayerRepositoryTest { private var mapConfigurator = StubMapConfigurator() private val repository = DirectoryReferenceLayerRepository( sharedLayersDir.absolutePath, - projectLayersDir.absolutePath, - mapConfigurator - ) + projectLayersDir.absolutePath + ) { mapConfigurator } @Test fun getAll_returnsAllSupportedLayersInTheDirectory() { diff --git a/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt index 389c24da318..45dcb88dc59 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt @@ -60,7 +60,7 @@ class MapFragmentReferenceLayerUtilsTest { assertNotNull( MapFragmentReferenceLayerUtils.getReferenceLayerFile( config, - DirectoryReferenceLayerRepository(layersPath, "", mapConfigurator) + DirectoryReferenceLayerRepository(layersPath, "") { mapConfigurator } ) ) } From 963ff0135aa64bbbac37b0fdd0563de945302b6e Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 24 Jun 2024 12:27:26 +0200 Subject: [PATCH 683/750] Improved tests --- .../DirectoryReferenceLayerRepositoryTest.kt | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index 9891c729fb4..0a7a3049213 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -84,6 +84,23 @@ class DirectoryReferenceLayerRepositoryTest { assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1)) } + @Test + fun getAllAlwaysUsesMapConfiguratorThatRepresentsTheCurrentConfiguration() { + val file = TempFiles.createTempFile(sharedLayersDir) + + mapConfigurator.apply { + addFile(file, false, file.name) + } + + assertThat(repository.getAll().isEmpty(), equalTo(true)) + + mapConfigurator = StubMapConfigurator().apply { + addFile(file, true, file.name) + } + + assertThat(repository.getAll().isEmpty(), equalTo(false)) + } + @Test fun get_returnsProperLayer() { val file1 = TempFiles.createTempFile(sharedLayersDir) @@ -160,6 +177,24 @@ class DirectoryReferenceLayerRepositoryTest { assertThat(repository.get(fileLayer.id)!!.name, equalTo(file.name)) } + @Test + fun getAlwaysUsesMapConfiguratorThatRepresentsTheCurrentConfiguration() { + val file = TempFiles.createTempFile(sharedLayersDir) + + mapConfigurator.apply { + addFile(file, false, file.name) + } + + val fileId = repository.getIdForFile(sharedLayersDir.absolutePath, file) + assertThat(repository.get(fileId), equalTo(null)) + + mapConfigurator = StubMapConfigurator().apply { + addFile(file, true, file.name) + } + + assertThat(repository.get(fileId)!!.file, equalTo(file)) + } + @Test fun addLayer_movesFileToTheSharedLayersDir_whenSharedIsTrue() { val file = TempFiles.createTempFile().also { @@ -205,23 +240,6 @@ class DirectoryReferenceLayerRepositoryTest { assertThat(repository.getAll(), contains(fileLayer2)) } - @Test // https://github.com/getodk/collect/issues/6211 - fun mapConfiguratorThatRepresentsTheCurrentConfigurationShouldBeUsedEveryTimeTheRepositoryIsCalled() { - val file = TempFiles.createTempFile(sharedLayersDir) - - mapConfigurator.apply { - addFile(file, false, file.name) - } - - assertThat(repository.getAll().isEmpty(), equalTo(true)) - - mapConfigurator = StubMapConfigurator().apply { - addFile(file, true, file.name) - } - - assertThat(repository.getAll().isEmpty(), equalTo(false)) - } - private class StubMapConfigurator : MapConfigurator { private val files = mutableMapOf>() From 4597084e5f88caa861a13198a3c4409a7b9b86a6 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 14 Jun 2024 11:44:23 +0100 Subject: [PATCH 684/750] Change select item type to string --- .../blankformlist/DeleteBlankFormFragment.kt | 8 ++-- .../savedformlist/DeleteSavedFormFragment.kt | 8 ++-- .../send/InstanceUploaderListActivity.java | 12 +++--- .../lists/multiselect/MultiSelectAdapter.kt | 2 +- .../MultiSelectControlsFragment.kt | 10 ++--- .../lists/multiselect/MultiSelectItem.kt | 2 +- .../lists/multiselect/MultiSelectViewModel.kt | 12 +++--- .../multiselect/MultiSelectAdapterTest.kt | 16 ++++---- .../MultiSelectControlsFragmentTest.kt | 6 +-- .../MultiSelectListFragmentTest.kt | 6 +-- .../multiselect/MultiSelectViewModelTest.kt | 38 +++++++++---------- 11 files changed, 60 insertions(+), 60 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt index 5ae957bcc77..798cd870ba5 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt @@ -35,7 +35,7 @@ class DeleteBlankFormFragment( blankFormListViewModel.formsToDisplay.map { it.map { blankForm -> MultiSelectItem( - blankForm.databaseId, + blankForm.databaseId.toString(), blankForm ) } @@ -66,7 +66,7 @@ class DeleteBlankFormFragment( MultiSelectControlsFragment.REQUEST_ACTION, this ) { _, result -> - val selected = result.getLongArray(MultiSelectControlsFragment.RESULT_SELECTED)!! + val selected = result.getStringArray(MultiSelectControlsFragment.RESULT_SELECTED)!! onDeleteSelected(selected) } } @@ -85,7 +85,7 @@ class DeleteBlankFormFragment( menuHost.addMenuProvider(blankFormListMenuProvider, viewLifecycleOwner, State.RESUMED) } - private fun onDeleteSelected(selected: LongArray) { + private fun onDeleteSelected(selected: Array) { MaterialAlertDialogBuilder(requireContext()) .setTitle(string.delete_file) .setMessage( @@ -95,7 +95,7 @@ class DeleteBlankFormFragment( ) ) .setPositiveButton(getString(string.delete_yes)) { _, _ -> - blankFormListViewModel.deleteForms(*selected) + blankFormListViewModel.deleteForms(*(selected.map { it.toLong() }.toLongArray())) multiSelectViewModel.unselectAll() } .setNegativeButton(getString(string.delete_no), null) diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt index 7479cf62d52..388a21ac53f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt @@ -38,7 +38,7 @@ class DeleteSavedFormFragment( savedFormListViewModel.formsToDisplay.map { it.map { instance -> MultiSelectItem( - instance.dbId, + instance.dbId.toString(), instance ) } @@ -69,7 +69,7 @@ class DeleteSavedFormFragment( MultiSelectControlsFragment.REQUEST_ACTION, this ) { _, result -> - val selected = result.getLongArray(MultiSelectControlsFragment.RESULT_SELECTED)!! + val selected = result.getStringArray(MultiSelectControlsFragment.RESULT_SELECTED)!! onDeleteSelected(selected) } } @@ -104,7 +104,7 @@ class DeleteSavedFormFragment( } } - private fun onDeleteSelected(selected: LongArray) { + private fun onDeleteSelected(selected: Array) { MaterialAlertDialogBuilder(requireContext()) .setTitle(string.delete_file) .setMessage( @@ -117,7 +117,7 @@ class DeleteSavedFormFragment( logDelete(selected.size) multiSelectViewModel.unselectAll() - savedFormListViewModel.deleteForms(selected).observe( + savedFormListViewModel.deleteForms(selected.map { it.toLong() }.toLongArray()).observe( viewLifecycleOwner, object : SnackbarPresenterObserver(requireView()) { override fun getSnackbarDetails(value: Int): SnackbarUtils.SnackbarDetails { 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 2850da8e6f2..6461a37cf43 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,10 +63,10 @@ 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.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; +import org.odk.collect.async.network.NetworkStateProvider; import org.odk.collect.lists.multiselect.MultiSelectViewModel; import org.odk.collect.settings.SettingsProvider; import org.odk.collect.settings.keys.ProjectKeys; @@ -75,6 +75,7 @@ import java.util.Arrays; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Inject; @@ -152,8 +153,7 @@ public void onCreate(Bundle savedInstanceState) { multiSelectViewModel.getSelected().observe(this, ids -> { binding.uploadButton.setEnabled(!ids.isEmpty()); allSelected = updateSelectAll(binding.toggleButton, listAdapter.getCount(), ids.size()); - - listAdapter.setSelected(ids); + listAdapter.setSelected(ids.stream().map(Long::valueOf).collect(Collectors.toSet())); }); readyToSendViewModel = new ViewModelProvider(this, factory).get(ReadyToSendViewModel.class); readyToSendViewModel.getData().observe(this, data -> binding.readyToSendBanner.setData(data)); @@ -179,7 +179,7 @@ public void onUploadButtonsClicked() { return; } - Set selectedItems = multiSelectViewModel.getSelected().getValue(); + Set selectedItems = multiSelectViewModel.getSelected().getValue().stream().map(Long::valueOf).collect(Collectors.toSet()); if (!selectedItems.isEmpty()) { binding.uploadButton.setEnabled(false); @@ -216,7 +216,7 @@ void init() { binding.toggleButton.setOnClickListener(v -> { if (!allSelected) { for (int i = 0; i < listView.getCount(); i++) { - multiSelectViewModel.select(listView.getItemIdAtPosition(i)); + multiSelectViewModel.select(String.valueOf(listView.getItemIdAtPosition(i))); } } else { multiSelectViewModel.unselectAll(); @@ -421,7 +421,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent intent) private void setupAdapter() { listAdapter = new InstanceUploaderAdapter(this, null, dbId -> { - multiSelectViewModel.toggle(dbId); + multiSelectViewModel.toggle(dbId.toString()); }); listView.setAdapter(listAdapter); diff --git a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectAdapter.kt b/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectAdapter.kt index 65a828c152b..863b6a38c20 100644 --- a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectAdapter.kt +++ b/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectAdapter.kt @@ -13,7 +13,7 @@ class MultiSelectAdapter>( private val viewHolderFactory: (ViewGroup) -> VH ) : RecyclerView.Adapter() { - var selected: Set = emptySet() + var selected: Set = emptySet() set(value) { field = value notifyDataSetChanged() diff --git a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectControlsFragment.kt b/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectControlsFragment.kt index 18dc45954d7..b305f3dcdae 100644 --- a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectControlsFragment.kt +++ b/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectControlsFragment.kt @@ -49,13 +49,13 @@ class MultiSelectControlsFragment( multiSelectViewModel.unselectAll() } - override fun onAction(selected: Set) { + override fun onAction(selected: Set) { parentFragmentManager.setFragmentResult( REQUEST_ACTION, Bundle().apply { - putLongArray( + putStringArray( RESULT_SELECTED, - selected.toLongArray() + selected.toTypedArray() ) } ) @@ -72,7 +72,7 @@ class MultiSelectControlsFragment( private class MultiSelectControlsView(context: Context) : FrameLayout(context) { - var selected = emptySet() + var selected = emptySet() set(value) { field = value render() @@ -121,6 +121,6 @@ private class MultiSelectControlsView(context: Context) : interface Listener { fun onSelectAll() fun onClearAll() - fun onAction(selected: Set) + fun onAction(selected: Set) } } diff --git a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectItem.kt b/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectItem.kt index f476474f4ef..9b07ea43112 100644 --- a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectItem.kt +++ b/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectItem.kt @@ -1,3 +1,3 @@ package org.odk.collect.lists.multiselect -data class MultiSelectItem(val id: Long, val item: T) +data class MultiSelectItem(val id: String, val item: T) diff --git a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectViewModel.kt b/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectViewModel.kt index e2131d5c01e..82a237cf9d4 100644 --- a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectViewModel.kt +++ b/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectViewModel.kt @@ -20,7 +20,7 @@ class MultiSelectViewModel( private val data: LiveData>> = MutableLiveData(emptyList()) ) : ViewModel() { - private val selected = MutableNonNullLiveData(emptySet()) + private val selected = MutableNonNullLiveData(emptySet()) private val isAllSelected = LiveDataUtils.zip(data, selected).map { (data, selected) -> data.isNotEmpty() && data.size == selected.size } @@ -29,15 +29,15 @@ class MultiSelectViewModel( return data } - fun select(item: Long) { + fun select(item: String) { updateSelected(selected.value + item) } - fun getSelected(): NonNullLiveData> { + fun getSelected(): NonNullLiveData> { return selected } - fun unselect(item: Long) { + fun unselect(item: String) { updateSelected(selected.value - item) } @@ -53,7 +53,7 @@ class MultiSelectViewModel( return isAllSelected } - fun toggle(item: Long) { + fun toggle(item: String) { if (selected.value.contains(item)) { unselect(item) } else { @@ -61,7 +61,7 @@ class MultiSelectViewModel( } } - private fun updateSelected(new: Set) { + private fun updateSelected(new: Set) { selected.value = new } diff --git a/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectAdapterTest.kt b/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectAdapterTest.kt index b7f6670cbc6..2579b120499 100644 --- a/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectAdapterTest.kt +++ b/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectAdapterTest.kt @@ -20,9 +20,9 @@ class MultiSelectAdapterTest { @Test fun `selected items are checked`() { - val data = MutableLiveData(listOf(MultiSelectItem(1, 1), MultiSelectItem(2, 2))) + val data = MutableLiveData(listOf(MultiSelectItem("1", 1), MultiSelectItem("2", 2))) val multiSelectViewModel = MultiSelectViewModel(data) - multiSelectViewModel.select(1) + multiSelectViewModel.select("1") val adapter = MultiSelectAdapter(multiSelectViewModel) { TextAndCheckBoxViewHolder(it.context) @@ -39,7 +39,7 @@ class MultiSelectAdapterTest { @Test fun `checking an item selects it`() { - val data = MutableLiveData(listOf(MultiSelectItem(1, 1), MultiSelectItem(2, 2))) + val data = MutableLiveData(listOf(MultiSelectItem("1", 1), MultiSelectItem("2", 2))) val multiSelectViewModel = MultiSelectViewModel(data) val adapter = MultiSelectAdapter(multiSelectViewModel) { @@ -51,12 +51,12 @@ class MultiSelectAdapterTest { val holders = createAndBindList(adapter) holders[0].view.checkBox.performClick() - assertThat(multiSelectViewModel.getSelected().value, equalTo(setOf(1))) + assertThat(multiSelectViewModel.getSelected().value, equalTo(setOf("1"))) } @Test fun `clicking an item selects it`() { - val data = MutableLiveData(listOf(MultiSelectItem(1, 1), MultiSelectItem(2, 2))) + val data = MutableLiveData(listOf(MultiSelectItem("1", 1), MultiSelectItem("2", 2))) val multiSelectViewModel = MultiSelectViewModel(data) val adapter = MultiSelectAdapter(multiSelectViewModel) { @@ -68,14 +68,14 @@ class MultiSelectAdapterTest { val holders = createAndBindList(adapter) holders[0].view.performClick() - assertThat(multiSelectViewModel.getSelected().value, equalTo(setOf(1))) + assertThat(multiSelectViewModel.getSelected().value, equalTo(setOf("1"))) } @Test fun `unchecking an item selects it`() { - val data = MutableLiveData(listOf(MultiSelectItem(1, 1), MultiSelectItem(2, 2))) + val data = MutableLiveData(listOf(MultiSelectItem("1", 1), MultiSelectItem("2", 2))) val multiSelectViewModel = MultiSelectViewModel(data) - multiSelectViewModel.select(1) + multiSelectViewModel.select("1") val adapter = MultiSelectAdapter(multiSelectViewModel) { TextAndCheckBoxViewHolder(it.context) diff --git a/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectControlsFragmentTest.kt b/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectControlsFragmentTest.kt index 0bd1df01dc9..ef3ff72adec 100644 --- a/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectControlsFragmentTest.kt +++ b/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectControlsFragmentTest.kt @@ -22,7 +22,7 @@ import org.odk.collect.strings.R.string @RunWith(AndroidJUnit4::class) class MultiSelectControlsFragmentTest { - private val data = MutableLiveData(listOf(MultiSelectItem(1, null), MultiSelectItem(2, null))) + private val data = MutableLiveData(listOf(MultiSelectItem("1", null), MultiSelectItem("2", null))) private val multiSelectViewModel = MultiSelectViewModel(data) @get:Rule @@ -38,7 +38,7 @@ class MultiSelectControlsFragmentTest { fragmentScenarioLauncherRule.launchInContainer(MultiSelectControlsFragment::class.java) onView(withText(string.select_all)).perform(click()) - assertThat(multiSelectViewModel.getSelected().getOrAwaitValue(), equalTo(setOf(1, 2))) + assertThat(multiSelectViewModel.getSelected().getOrAwaitValue(), equalTo(setOf("1", "2"))) } @Test @@ -57,7 +57,7 @@ class MultiSelectControlsFragmentTest { fragmentScenarioLauncherRule.launchInContainer(MultiSelectControlsFragment::class.java) onView(withText("Action")).check(matches(not(isEnabled()))) - multiSelectViewModel.select(1) + multiSelectViewModel.select("1") onView(withText("Action")).check(matches(isEnabled())) } } diff --git a/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectListFragmentTest.kt b/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectListFragmentTest.kt index bc44d8bb425..9c3079325fb 100644 --- a/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectListFragmentTest.kt +++ b/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectListFragmentTest.kt @@ -44,7 +44,7 @@ class MultiSelectListFragmentTest { fragmentScenarioLauncherRule.launchInContainer(MultiSelectListFragment::class.java) onView(withId(R.id.empty)).check(matches(isDisplayed())) - data.value = listOf(MultiSelectItem(1, "Blah")) + data.value = listOf(MultiSelectItem("1", "Blah")) onView(withId(R.id.empty)).check(matches(not(isDisplayed()))) } @@ -53,7 +53,7 @@ class MultiSelectListFragmentTest { fragmentScenarioLauncherRule.launchInContainer(MultiSelectListFragment::class.java) onView(withId(R.id.buttons)).check(matches(not(isDisplayed()))) - data.value = listOf(MultiSelectItem(1, "Blah")) + data.value = listOf(MultiSelectItem("1", "Blah")) onView(withId(R.id.buttons)).check(matches(isDisplayed())) } @@ -61,7 +61,7 @@ class MultiSelectListFragmentTest { fun `recreating maintains selection`() { val scenario = fragmentScenarioLauncherRule.launchInContainer(MultiSelectListFragment::class.java) - data.value = listOf(MultiSelectItem(1, "Blah 1"), MultiSelectItem(2, "Blah 2")) + data.value = listOf(MultiSelectItem("1", "Blah 1"), MultiSelectItem("1", "Blah 2")) onView(recyclerView()).perform(clickOnItemWith(withText("Blah 2"))) diff --git a/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectViewModelTest.kt b/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectViewModelTest.kt index 7e47c89cf0e..d1d1fdfe0a1 100644 --- a/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectViewModelTest.kt +++ b/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectViewModelTest.kt @@ -16,27 +16,27 @@ class MultiSelectViewModelTest { @Test fun `getSelected returns selected`() { val viewModel = MultiSelectViewModel() - viewModel.select(1) - viewModel.select(11) + viewModel.select("1") + viewModel.select("11") - assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf(1, 11))) + assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf("1", "11"))) } @Test fun `getSelected does not return unselected items`() { val viewModel = MultiSelectViewModel() - viewModel.select(1) - viewModel.select(11) - viewModel.unselect(1) + viewModel.select("1") + viewModel.select("11") + viewModel.unselect("1") - assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf(11))) + assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf("11"))) } @Test fun `unselectAll unselects all items`() { val viewModel = MultiSelectViewModel() - viewModel.select(1) - viewModel.select(11) + viewModel.select("1") + viewModel.select("11") viewModel.unselectAll() assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(emptySet())) @@ -46,33 +46,33 @@ class MultiSelectViewModelTest { fun `toggle changes item back and forth`() { val viewModel = MultiSelectViewModel() - viewModel.toggle(1) - viewModel.toggle(11) - assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf(1, 11))) + viewModel.toggle("1") + viewModel.toggle("11") + assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf("1", "11"))) - viewModel.toggle(11) - assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf(1))) + viewModel.toggle("11") + assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf("1"))) } @Test fun `selectAll selects all data`() { - val data = MutableLiveData(listOf(MultiSelectItem(1, 1), MultiSelectItem(2, 2))) + val data = MutableLiveData(listOf(MultiSelectItem("1", 1), MultiSelectItem("2", 2))) val viewModel = MultiSelectViewModel(data) viewModel.selectAll() - assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf(1, 2))) + assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf("1", "2"))) } @Test fun `isAllSelected is true when all data selected`() { - val data = MutableLiveData(listOf(MultiSelectItem(1, 1), MultiSelectItem(2, 2))) + val data = MutableLiveData(listOf(MultiSelectItem("1", 1), MultiSelectItem("2", 2))) val viewModel = MultiSelectViewModel(data) assertThat(viewModel.isAllSelected().getOrAwaitValue(), equalTo(false)) - viewModel.select(1) + viewModel.select("1") assertThat(viewModel.isAllSelected().getOrAwaitValue(), equalTo(false)) - viewModel.select(2) + viewModel.select("2") assertThat(viewModel.isAllSelected().getOrAwaitValue(), equalTo(true)) } From 8a3684bed93a322e118a20f71157f3cd8aca0ce4 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 14 Jun 2024 11:49:20 +0100 Subject: [PATCH 685/750] Rename package to get ready for single select code --- .../blankformlist/DeleteBlankFormFragment.kt | 12 ++++++------ .../savedformlist/DeleteSavedFormFragment.kt | 10 +++++----- .../SelectableSavedFormListItemViewHolder.kt | 2 +- .../send/InstanceUploaderListActivity.java | 4 ++-- .../src/main/res/layout/delete_form_layout.xml | 2 +- .../collect/lists/multiselect/MultiSelectItem.kt | 3 --- .../{multiselect => selects}/MultiSelectAdapter.kt | 4 ++-- .../MultiSelectControlsFragment.kt | 2 +- .../MultiSelectListFragment.kt | 2 +- .../MultiSelectViewModel.kt | 8 ++++---- .../org/odk/collect/lists/selects/SelectItem.kt | 3 +++ lists/src/main/res/layout/multi_select_list.xml | 2 +- .../MultiSelectAdapterTest.kt | 12 ++++++------ .../MultiSelectControlsFragmentTest.kt | 4 ++-- .../MultiSelectListFragmentTest.kt | 14 +++++++------- .../MultiSelectViewModelTest.kt | 8 ++++---- .../support/TextAndCheckboxViewHolder.kt | 4 ++-- 17 files changed, 48 insertions(+), 48 deletions(-) delete mode 100644 lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectItem.kt rename lists/src/main/java/org/odk/collect/lists/{multiselect => selects}/MultiSelectAdapter.kt (94%) rename lists/src/main/java/org/odk/collect/lists/{multiselect => selects}/MultiSelectControlsFragment.kt (98%) rename lists/src/main/java/org/odk/collect/lists/{multiselect => selects}/MultiSelectListFragment.kt (98%) rename lists/src/main/java/org/odk/collect/lists/{multiselect => selects}/MultiSelectViewModel.kt (88%) create mode 100644 lists/src/main/java/org/odk/collect/lists/selects/SelectItem.kt rename lists/src/test/java/org/odk/collect/lists/{multiselect => selects}/MultiSelectAdapterTest.kt (86%) rename lists/src/test/java/org/odk/collect/lists/{multiselect => selects}/MultiSelectControlsFragmentTest.kt (94%) rename lists/src/test/java/org/odk/collect/lists/{multiselect => selects}/MultiSelectListFragmentTest.kt (84%) rename lists/src/test/java/org/odk/collect/lists/{multiselect => selects}/MultiSelectViewModelTest.kt (88%) rename lists/src/test/java/org/odk/collect/lists/{multiselect => selects}/support/TextAndCheckboxViewHolder.kt (88%) diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt index 798cd870ba5..47c3a6cfa37 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt @@ -17,11 +17,11 @@ import org.odk.collect.android.R import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.lists.RecyclerViewUtils import org.odk.collect.lists.RecyclerViewUtils.matchParentWidth -import org.odk.collect.lists.multiselect.MultiSelectAdapter -import org.odk.collect.lists.multiselect.MultiSelectControlsFragment -import org.odk.collect.lists.multiselect.MultiSelectItem -import org.odk.collect.lists.multiselect.MultiSelectListFragment -import org.odk.collect.lists.multiselect.MultiSelectViewModel +import org.odk.collect.lists.selects.MultiSelectAdapter +import org.odk.collect.lists.selects.MultiSelectControlsFragment +import org.odk.collect.lists.selects.MultiSelectListFragment +import org.odk.collect.lists.selects.MultiSelectViewModel +import org.odk.collect.lists.selects.SelectItem import org.odk.collect.strings.R.string class DeleteBlankFormFragment( @@ -34,7 +34,7 @@ class DeleteBlankFormFragment( MultiSelectViewModel.Factory( blankFormListViewModel.formsToDisplay.map { it.map { blankForm -> - MultiSelectItem( + SelectItem( blankForm.databaseId.toString(), blankForm ) diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt index 388a21ac53f..86a07d7d532 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt @@ -20,10 +20,10 @@ import org.odk.collect.androidshared.ui.SnackbarUtils import org.odk.collect.androidshared.ui.SnackbarUtils.SnackbarPresenterObserver import org.odk.collect.forms.instances.Instance import org.odk.collect.lists.RecyclerViewUtils -import org.odk.collect.lists.multiselect.MultiSelectControlsFragment -import org.odk.collect.lists.multiselect.MultiSelectItem -import org.odk.collect.lists.multiselect.MultiSelectListFragment -import org.odk.collect.lists.multiselect.MultiSelectViewModel +import org.odk.collect.lists.selects.MultiSelectControlsFragment +import org.odk.collect.lists.selects.MultiSelectListFragment +import org.odk.collect.lists.selects.MultiSelectViewModel +import org.odk.collect.lists.selects.SelectItem import org.odk.collect.material.MaterialProgressDialogFragment import org.odk.collect.strings.R.string @@ -37,7 +37,7 @@ class DeleteSavedFormFragment( MultiSelectViewModel.Factory( savedFormListViewModel.formsToDisplay.map { it.map { instance -> - MultiSelectItem( + SelectItem( instance.dbId.toString(), instance ) diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/SelectableSavedFormListItemViewHolder.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/SelectableSavedFormListItemViewHolder.kt index 013f8f89ae0..8e08ee23837 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/SelectableSavedFormListItemViewHolder.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/SelectableSavedFormListItemViewHolder.kt @@ -6,7 +6,7 @@ import android.widget.CheckBox import org.odk.collect.android.R import org.odk.collect.forms.instances.Instance import org.odk.collect.lists.RecyclerViewUtils.matchParentWidth -import org.odk.collect.lists.multiselect.MultiSelectAdapter +import org.odk.collect.lists.selects.MultiSelectAdapter class SelectableSavedFormListItemViewHolder(parent: ViewGroup) : MultiSelectAdapter.ViewHolder( 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 6461a37cf43..de51332ac52 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 @@ -20,7 +20,7 @@ import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_DATE_DESC; import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_NAME_ASC; import static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_NAME_DESC; -import static org.odk.collect.lists.multiselect.MultiSelectViewModelKt.updateSelectAll; +import static org.odk.collect.lists.selects.MultiSelectViewModelKt.updateSelectAll; import android.content.Intent; import android.database.Cursor; @@ -67,7 +67,7 @@ import org.odk.collect.androidshared.ui.ToastUtils; import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard; import org.odk.collect.async.network.NetworkStateProvider; -import org.odk.collect.lists.multiselect.MultiSelectViewModel; +import org.odk.collect.lists.selects.MultiSelectViewModel; import org.odk.collect.settings.SettingsProvider; import org.odk.collect.settings.keys.ProjectKeys; import org.odk.collect.strings.localization.LocalizedActivity; diff --git a/collect_app/src/main/res/layout/delete_form_layout.xml b/collect_app/src/main/res/layout/delete_form_layout.xml index b13361bd58d..7505e92a5ec 100644 --- a/collect_app/src/main/res/layout/delete_form_layout.xml +++ b/collect_app/src/main/res/layout/delete_form_layout.xml @@ -6,7 +6,7 @@ (val id: String, val item: T) diff --git a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectAdapter.kt b/lists/src/main/java/org/odk/collect/lists/selects/MultiSelectAdapter.kt similarity index 94% rename from lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectAdapter.kt rename to lists/src/main/java/org/odk/collect/lists/selects/MultiSelectAdapter.kt index 863b6a38c20..b08c6c1e3ce 100644 --- a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectAdapter.kt +++ b/lists/src/main/java/org/odk/collect/lists/selects/MultiSelectAdapter.kt @@ -1,4 +1,4 @@ -package org.odk.collect.lists.multiselect +package org.odk.collect.lists.selects import android.view.View import android.view.ViewGroup @@ -19,7 +19,7 @@ class MultiSelectAdapter>( notifyDataSetChanged() } - var data = emptyList>() + var data = emptyList>() set(value) { field = value.toList() notifyDataSetChanged() diff --git a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectControlsFragment.kt b/lists/src/main/java/org/odk/collect/lists/selects/MultiSelectControlsFragment.kt similarity index 98% rename from lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectControlsFragment.kt rename to lists/src/main/java/org/odk/collect/lists/selects/MultiSelectControlsFragment.kt index b305f3dcdae..eb41edb7f69 100644 --- a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectControlsFragment.kt +++ b/lists/src/main/java/org/odk/collect/lists/selects/MultiSelectControlsFragment.kt @@ -1,4 +1,4 @@ -package org.odk.collect.lists.multiselect +package org.odk.collect.lists.selects import android.content.Context import android.os.Bundle diff --git a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectListFragment.kt b/lists/src/main/java/org/odk/collect/lists/selects/MultiSelectListFragment.kt similarity index 98% rename from lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectListFragment.kt rename to lists/src/main/java/org/odk/collect/lists/selects/MultiSelectListFragment.kt index 4ed9a6aee3c..389cc356d21 100644 --- a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectListFragment.kt +++ b/lists/src/main/java/org/odk/collect/lists/selects/MultiSelectListFragment.kt @@ -1,4 +1,4 @@ -package org.odk.collect.lists.multiselect +package org.odk.collect.lists.selects import android.content.Context import android.os.Bundle diff --git a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectViewModel.kt b/lists/src/main/java/org/odk/collect/lists/selects/MultiSelectViewModel.kt similarity index 88% rename from lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectViewModel.kt rename to lists/src/main/java/org/odk/collect/lists/selects/MultiSelectViewModel.kt index 82a237cf9d4..57f284d2127 100644 --- a/lists/src/main/java/org/odk/collect/lists/multiselect/MultiSelectViewModel.kt +++ b/lists/src/main/java/org/odk/collect/lists/selects/MultiSelectViewModel.kt @@ -1,4 +1,4 @@ -package org.odk.collect.lists.multiselect +package org.odk.collect.lists.selects import android.widget.Button import androidx.lifecycle.LiveData @@ -17,7 +17,7 @@ import org.odk.collect.androidshared.livedata.NonNullLiveData * all and determine whether all items are selected or not. */ class MultiSelectViewModel( - private val data: LiveData>> = MutableLiveData(emptyList()) + private val data: LiveData>> = MutableLiveData(emptyList()) ) : ViewModel() { private val selected = MutableNonNullLiveData(emptySet()) @@ -25,7 +25,7 @@ class MultiSelectViewModel( data.isNotEmpty() && data.size == selected.size } - fun getData(): LiveData>> { + fun getData(): LiveData>> { return data } @@ -65,7 +65,7 @@ class MultiSelectViewModel( selected.value = new } - class Factory(private val data: LiveData>>) : ViewModelProvider.Factory { + class Factory(private val data: LiveData>>) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class, extras: CreationExtras): VM { return MultiSelectViewModel(data) as VM diff --git a/lists/src/main/java/org/odk/collect/lists/selects/SelectItem.kt b/lists/src/main/java/org/odk/collect/lists/selects/SelectItem.kt new file mode 100644 index 00000000000..e3b0fc27825 --- /dev/null +++ b/lists/src/main/java/org/odk/collect/lists/selects/SelectItem.kt @@ -0,0 +1,3 @@ +package org.odk.collect.lists.selects + +data class SelectItem(val id: String, val item: T) diff --git a/lists/src/main/res/layout/multi_select_list.xml b/lists/src/main/res/layout/multi_select_list.xml index e126afff2cb..af7e8e06329 100644 --- a/lists/src/main/res/layout/multi_select_list.xml +++ b/lists/src/main/res/layout/multi_select_list.xml @@ -19,7 +19,7 @@ ("1", 1), MultiSelectItem("2", 2))) + val data = MutableLiveData(listOf(SelectItem("1", 1), SelectItem("2", 2))) val multiSelectViewModel = MultiSelectViewModel(data) multiSelectViewModel.select("1") @@ -39,7 +39,7 @@ class MultiSelectAdapterTest { @Test fun `checking an item selects it`() { - val data = MutableLiveData(listOf(MultiSelectItem("1", 1), MultiSelectItem("2", 2))) + val data = MutableLiveData(listOf(SelectItem("1", 1), SelectItem("2", 2))) val multiSelectViewModel = MultiSelectViewModel(data) val adapter = MultiSelectAdapter(multiSelectViewModel) { @@ -56,7 +56,7 @@ class MultiSelectAdapterTest { @Test fun `clicking an item selects it`() { - val data = MutableLiveData(listOf(MultiSelectItem("1", 1), MultiSelectItem("2", 2))) + val data = MutableLiveData(listOf(SelectItem("1", 1), SelectItem("2", 2))) val multiSelectViewModel = MultiSelectViewModel(data) val adapter = MultiSelectAdapter(multiSelectViewModel) { @@ -73,7 +73,7 @@ class MultiSelectAdapterTest { @Test fun `unchecking an item selects it`() { - val data = MutableLiveData(listOf(MultiSelectItem("1", 1), MultiSelectItem("2", 2))) + val data = MutableLiveData(listOf(SelectItem("1", 1), SelectItem("2", 2))) val multiSelectViewModel = MultiSelectViewModel(data) multiSelectViewModel.select("1") diff --git a/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectControlsFragmentTest.kt b/lists/src/test/java/org/odk/collect/lists/selects/MultiSelectControlsFragmentTest.kt similarity index 94% rename from lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectControlsFragmentTest.kt rename to lists/src/test/java/org/odk/collect/lists/selects/MultiSelectControlsFragmentTest.kt index ef3ff72adec..58cd769537c 100644 --- a/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectControlsFragmentTest.kt +++ b/lists/src/test/java/org/odk/collect/lists/selects/MultiSelectControlsFragmentTest.kt @@ -1,4 +1,4 @@ -package org.odk.collect.lists.multiselect +package org.odk.collect.lists.selects import androidx.lifecycle.MutableLiveData import androidx.test.espresso.Espresso.onView @@ -22,7 +22,7 @@ import org.odk.collect.strings.R.string @RunWith(AndroidJUnit4::class) class MultiSelectControlsFragmentTest { - private val data = MutableLiveData(listOf(MultiSelectItem("1", null), MultiSelectItem("2", null))) + private val data = MutableLiveData(listOf(SelectItem("1", null), SelectItem("2", null))) private val multiSelectViewModel = MultiSelectViewModel(data) @get:Rule diff --git a/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectListFragmentTest.kt b/lists/src/test/java/org/odk/collect/lists/selects/MultiSelectListFragmentTest.kt similarity index 84% rename from lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectListFragmentTest.kt rename to lists/src/test/java/org/odk/collect/lists/selects/MultiSelectListFragmentTest.kt index 9c3079325fb..31e5a89194a 100644 --- a/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectListFragmentTest.kt +++ b/lists/src/test/java/org/odk/collect/lists/selects/MultiSelectListFragmentTest.kt @@ -1,4 +1,4 @@ -package org.odk.collect.lists.multiselect +package org.odk.collect.lists.selects import androidx.lifecycle.MutableLiveData import androidx.test.espresso.Espresso.onView @@ -15,8 +15,8 @@ import org.junit.runner.RunWith import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule import org.odk.collect.lists.R -import org.odk.collect.lists.multiselect.support.TextAndCheckBoxView -import org.odk.collect.lists.multiselect.support.TextAndCheckBoxViewHolder +import org.odk.collect.lists.selects.support.TextAndCheckBoxView +import org.odk.collect.lists.selects.support.TextAndCheckBoxViewHolder import org.odk.collect.testshared.RecyclerViewMatcher.Companion.withRecyclerView import org.odk.collect.testshared.ViewActions.clickOnItemWith import org.odk.collect.testshared.ViewMatchers.recyclerView @@ -24,7 +24,7 @@ import org.odk.collect.testshared.ViewMatchers.recyclerView @RunWith(AndroidJUnit4::class) class MultiSelectListFragmentTest { - private val data = MutableLiveData>>(emptyList()) + private val data = MutableLiveData>>(emptyList()) private val multiSelectViewModel = MultiSelectViewModel(data) @get:Rule @@ -44,7 +44,7 @@ class MultiSelectListFragmentTest { fragmentScenarioLauncherRule.launchInContainer(MultiSelectListFragment::class.java) onView(withId(R.id.empty)).check(matches(isDisplayed())) - data.value = listOf(MultiSelectItem("1", "Blah")) + data.value = listOf(SelectItem("1", "Blah")) onView(withId(R.id.empty)).check(matches(not(isDisplayed()))) } @@ -53,7 +53,7 @@ class MultiSelectListFragmentTest { fragmentScenarioLauncherRule.launchInContainer(MultiSelectListFragment::class.java) onView(withId(R.id.buttons)).check(matches(not(isDisplayed()))) - data.value = listOf(MultiSelectItem("1", "Blah")) + data.value = listOf(SelectItem("1", "Blah")) onView(withId(R.id.buttons)).check(matches(isDisplayed())) } @@ -61,7 +61,7 @@ class MultiSelectListFragmentTest { fun `recreating maintains selection`() { val scenario = fragmentScenarioLauncherRule.launchInContainer(MultiSelectListFragment::class.java) - data.value = listOf(MultiSelectItem("1", "Blah 1"), MultiSelectItem("1", "Blah 2")) + data.value = listOf(SelectItem("1", "Blah 1"), SelectItem("1", "Blah 2")) onView(recyclerView()).perform(clickOnItemWith(withText("Blah 2"))) diff --git a/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectViewModelTest.kt b/lists/src/test/java/org/odk/collect/lists/selects/MultiSelectViewModelTest.kt similarity index 88% rename from lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectViewModelTest.kt rename to lists/src/test/java/org/odk/collect/lists/selects/MultiSelectViewModelTest.kt index d1d1fdfe0a1..75b53d34931 100644 --- a/lists/src/test/java/org/odk/collect/lists/multiselect/MultiSelectViewModelTest.kt +++ b/lists/src/test/java/org/odk/collect/lists/selects/MultiSelectViewModelTest.kt @@ -1,4 +1,4 @@ -package org.odk.collect.lists.multiselect +package org.odk.collect.lists.selects import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.MutableLiveData @@ -56,7 +56,7 @@ class MultiSelectViewModelTest { @Test fun `selectAll selects all data`() { - val data = MutableLiveData(listOf(MultiSelectItem("1", 1), MultiSelectItem("2", 2))) + val data = MutableLiveData(listOf(SelectItem("1", 1), SelectItem("2", 2))) val viewModel = MultiSelectViewModel(data) viewModel.selectAll() @@ -65,7 +65,7 @@ class MultiSelectViewModelTest { @Test fun `isAllSelected is true when all data selected`() { - val data = MutableLiveData(listOf(MultiSelectItem("1", 1), MultiSelectItem("2", 2))) + val data = MutableLiveData(listOf(SelectItem("1", 1), SelectItem("2", 2))) val viewModel = MultiSelectViewModel(data) assertThat(viewModel.isAllSelected().getOrAwaitValue(), equalTo(false)) @@ -78,7 +78,7 @@ class MultiSelectViewModelTest { @Test fun `isAllSelected returns false when no data`() { - val data = MutableLiveData(listOf>()) + val data = MutableLiveData(listOf>()) val viewModel = MultiSelectViewModel(data) assertThat(viewModel.isAllSelected().getOrAwaitValue(), equalTo(false)) } diff --git a/lists/src/test/java/org/odk/collect/lists/multiselect/support/TextAndCheckboxViewHolder.kt b/lists/src/test/java/org/odk/collect/lists/selects/support/TextAndCheckboxViewHolder.kt similarity index 88% rename from lists/src/test/java/org/odk/collect/lists/multiselect/support/TextAndCheckboxViewHolder.kt rename to lists/src/test/java/org/odk/collect/lists/selects/support/TextAndCheckboxViewHolder.kt index 7b18135fd24..35395438b83 100644 --- a/lists/src/test/java/org/odk/collect/lists/multiselect/support/TextAndCheckboxViewHolder.kt +++ b/lists/src/test/java/org/odk/collect/lists/selects/support/TextAndCheckboxViewHolder.kt @@ -1,10 +1,10 @@ -package org.odk.collect.lists.multiselect.support +package org.odk.collect.lists.selects.support import android.content.Context import android.widget.CheckBox import android.widget.FrameLayout import android.widget.TextView -import org.odk.collect.lists.multiselect.MultiSelectAdapter +import org.odk.collect.lists.selects.MultiSelectAdapter class TextAndCheckBoxView(context: Context) : FrameLayout(context) { From 07a0a976267cb2910e0dea375e263295e5436d09 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 14 Jun 2024 13:08:14 +0100 Subject: [PATCH 686/750] Use multi select for expansion state --- maps/build.gradle.kts | 1 + .../maps/layers/OfflineMapLayersPicker.kt | 16 ++++++++++++---- .../maps/layers/OfflineMapLayersPickerAdapter.kt | 6 ++++-- .../layers/OfflineMapLayersStateViewModel.kt | 13 ------------- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/maps/build.gradle.kts b/maps/build.gradle.kts index 81b6d7ab8b1..cf2d40c9326 100644 --- a/maps/build.gradle.kts +++ b/maps/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(project(":strings")) implementation(project(":web-page")) implementation(project(":analytics")) + implementation(project(":lists")) implementation(Dependencies.android_material) implementation(Dependencies.kotlin_stdlib) implementation(Dependencies.androidx_fragment_ktx) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 11ade230115..9752a932d35 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -11,6 +11,7 @@ import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.viewModelFactory import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -19,6 +20,7 @@ import org.odk.collect.androidshared.ui.DialogFragmentUtils import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.androidshared.ui.addOnClickListener import org.odk.collect.async.Scheduler +import org.odk.collect.lists.selects.MultiSelectViewModel import org.odk.collect.maps.databinding.OfflineMapLayersPickerBinding import org.odk.collect.settings.SettingsProvider import org.odk.collect.strings.localization.getLocalizedString @@ -48,6 +50,12 @@ class OfflineMapLayersPicker( } } + private val multiSelectViewModel: MultiSelectViewModel<*> by viewModels { + viewModelFactory { + addInitializer(MultiSelectViewModel::class) { MultiSelectViewModel() } + } + } + private val getLayers = registerForActivityResult(ActivityResultContracts.GetMultipleContents(), registry) { uris -> if (uris.isNotEmpty()) { sharedViewModel.loadLayersToImport(uris, requireContext()) @@ -118,9 +126,9 @@ class OfflineMapLayersPicker( LiveDataUtils.zip3( sharedViewModel.existingLayers, stateViewModel.checkedLayerId, - stateViewModel.expandedLayerIds + multiSelectViewModel.getSelected() ).observe(this) { (layers, checkedLayerId, expandedLayerIds) -> - updateAdapter(layers, checkedLayerId, expandedLayerIds, adapter) + updateAdapter(layers, checkedLayerId, expandedLayerIds.toList(), adapter) } sharedViewModel.existingLayers.observe(this) { layers -> @@ -143,8 +151,8 @@ class OfflineMapLayersPicker( stateViewModel.onLayerChecked(layerId) } - override fun onLayerToggled(layerId: String?) { - stateViewModel.onLayerToggled(layerId) + override fun onLayerToggled(layerId: String) { + multiSelectViewModel.toggle(layerId) } override fun onDeleteLayer(layerItem: CheckableReferenceLayer) { diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt index 059c4261bdf..ca6be54f03c 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt @@ -17,7 +17,7 @@ class OfflineMapLayersPickerAdapter( ) : RecyclerView.Adapter() { interface OfflineMapLayersPickerAdapterInterface { fun onLayerChecked(layerId: String?) - fun onLayerToggled(layerId: String?) + fun onLayerToggled(layerId: String) fun onDeleteLayer(layerItem: CheckableReferenceLayer) } @@ -60,7 +60,9 @@ class OfflineMapLayersPickerAdapter( } holder.binding.arrow.setOnClickListener { - listener.onLayerToggled(layer.id) + if (layer.id != null) { + listener.onLayerToggled(layer.id) + } } holder.binding.deleteLayer.setOnClickListener { diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt index c6f84407b90..5d4d1521c59 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt @@ -3,17 +3,12 @@ package org.odk.collect.maps.layers import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import org.odk.collect.androidshared.livedata.MutableNonNullLiveData -import org.odk.collect.androidshared.livedata.NonNullLiveData import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.keys.ProjectKeys class OfflineMapLayersStateViewModel( private val settingsProvider: SettingsProvider ) : ViewModel() { - private val _expandedLayerIds = MutableNonNullLiveData>(emptyList()) - val expandedLayerIds: NonNullLiveData> = _expandedLayerIds - private val _checkedLayerId = MutableLiveData(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER)) val checkedLayerId: LiveData = _checkedLayerId @@ -22,14 +17,6 @@ class OfflineMapLayersStateViewModel( _checkedLayerId.value = layerId } - fun onLayerToggled(layerId: String?) { - if (_expandedLayerIds.value.contains(layerId)) { - _expandedLayerIds.value = _expandedLayerIds.value.filter { it != layerId } - } else { - _expandedLayerIds.value = _expandedLayerIds.value.plus(layerId) - } - } - fun onLayersChanged(layerIds: List) { if (!layerIds.contains(_checkedLayerId.value)) { _checkedLayerId.value = null From b0da644750d737ccbe88809c87507e99a0db4cbc Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 14 Jun 2024 13:27:04 +0100 Subject: [PATCH 687/750] Create generic single select view model --- .../odk/collect/lists/selects/SelectItem.kt | 2 +- .../lists/selects/SingleSelectViewModel.kt | 31 ++++++++++ .../maps/layers/OfflineMapLayersPicker.kt | 61 +++++++++++++------ .../layers/OfflineMapLayersStateViewModel.kt | 30 --------- .../maps/layers/OfflineMapLayersViewModel.kt | 4 ++ 5 files changed, 78 insertions(+), 50 deletions(-) create mode 100644 lists/src/main/java/org/odk/collect/lists/selects/SingleSelectViewModel.kt delete mode 100644 maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt diff --git a/lists/src/main/java/org/odk/collect/lists/selects/SelectItem.kt b/lists/src/main/java/org/odk/collect/lists/selects/SelectItem.kt index e3b0fc27825..75dc3b7a573 100644 --- a/lists/src/main/java/org/odk/collect/lists/selects/SelectItem.kt +++ b/lists/src/main/java/org/odk/collect/lists/selects/SelectItem.kt @@ -1,3 +1,3 @@ package org.odk.collect.lists.selects -data class SelectItem(val id: String, val item: T) +data class SelectItem(val id: String, val item: T, val selected: Boolean = false) diff --git a/lists/src/main/java/org/odk/collect/lists/selects/SingleSelectViewModel.kt b/lists/src/main/java/org/odk/collect/lists/selects/SingleSelectViewModel.kt new file mode 100644 index 00000000000..677f422812e --- /dev/null +++ b/lists/src/main/java/org/odk/collect/lists/selects/SingleSelectViewModel.kt @@ -0,0 +1,31 @@ +package org.odk.collect.lists.selects + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.map +import org.odk.collect.androidshared.livedata.LiveDataUtils + +class SingleSelectViewModel(private val data: LiveData>>) : ViewModel() { + + private val _selected = MutableLiveData(null) + private val selected = LiveDataUtils.zip(_selected, data).map { (selected, data) -> + if (selected != null && data.any { it.id == selected }) { + selected + } else { + data.find { it.selected }?.id + } + } + + fun getSelected(): LiveData { + return selected + } + + fun select(item: String) { + _selected.value = item + } + + fun clear() { + _selected.value = null + } +} diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 9752a932d35..0a51a77e746 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -11,6 +11,7 @@ import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.map import androidx.lifecycle.viewmodel.viewModelFactory import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -21,8 +22,11 @@ import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.androidshared.ui.addOnClickListener import org.odk.collect.async.Scheduler import org.odk.collect.lists.selects.MultiSelectViewModel +import org.odk.collect.lists.selects.SelectItem +import org.odk.collect.lists.selects.SingleSelectViewModel import org.odk.collect.maps.databinding.OfflineMapLayersPickerBinding import org.odk.collect.settings.SettingsProvider +import org.odk.collect.settings.keys.ProjectKeys import org.odk.collect.strings.localization.getLocalizedString import org.odk.collect.webpage.ExternalWebPageHelper @@ -34,28 +38,42 @@ class OfflineMapLayersPicker( private val externalWebPageHelper: ExternalWebPageHelper ) : BottomSheetDialogFragment(), OfflineMapLayersPickerAdapter.OfflineMapLayersPickerAdapterInterface { - private val stateViewModel: OfflineMapLayersStateViewModel by viewModels { - object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return OfflineMapLayersStateViewModel(settingsProvider) as T - } - } - } private val sharedViewModel: OfflineMapLayersViewModel by activityViewModels { object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider) as T + return OfflineMapLayersViewModel( + referenceLayerRepository, + scheduler, + settingsProvider + ) as T } } } - private val multiSelectViewModel: MultiSelectViewModel<*> by viewModels { + private val expandedStateViewModel: MultiSelectViewModel<*> by viewModels { viewModelFactory { addInitializer(MultiSelectViewModel::class) { MultiSelectViewModel() } } } + private val checkedStateViewModel: SingleSelectViewModel by viewModels { + viewModelFactory { + addInitializer(SingleSelectViewModel::class) { + SingleSelectViewModel(sharedViewModel.existingLayers.map { + it.map { layer -> + SelectItem( + layer.id, + layer, + settingsProvider.getUnprotectedSettings() + .getString(ProjectKeys.KEY_REFERENCE_LAYER) == layer.id + ) + } + }) + } + } + } + private val getLayers = registerForActivityResult(ActivityResultContracts.GetMultipleContents(), registry) { uris -> if (uris.isNotEmpty()) { sharedViewModel.loadLayersToImport(uris, requireContext()) @@ -99,7 +117,7 @@ class OfflineMapLayersPicker( } binding.save.setOnClickListener { - sharedViewModel.saveCheckedLayer(stateViewModel.getCheckedLayer()) + sharedViewModel.saveCheckedLayer(checkedStateViewModel.getSelected().value) dismiss() } @@ -125,15 +143,11 @@ class OfflineMapLayersPicker( binding.layers.setAdapter(adapter) LiveDataUtils.zip3( sharedViewModel.existingLayers, - stateViewModel.checkedLayerId, - multiSelectViewModel.getSelected() + checkedStateViewModel.getSelected(), + expandedStateViewModel.getSelected() ).observe(this) { (layers, checkedLayerId, expandedLayerIds) -> updateAdapter(layers, checkedLayerId, expandedLayerIds.toList(), adapter) } - - sharedViewModel.existingLayers.observe(this) { layers -> - stateViewModel.onLayersChanged(layers.map { it.id }.plus(null)) - } } override fun onStart() { @@ -148,16 +162,25 @@ class OfflineMapLayersPicker( } override fun onLayerChecked(layerId: String?) { - stateViewModel.onLayerChecked(layerId) + if (layerId != null) { + checkedStateViewModel.select(layerId) + } else { + checkedStateViewModel.clear() + } } override fun onLayerToggled(layerId: String) { - multiSelectViewModel.toggle(layerId) + expandedStateViewModel.toggle(layerId) } override fun onDeleteLayer(layerItem: CheckableReferenceLayer) { MaterialAlertDialogBuilder(requireActivity()) - .setMessage(requireActivity().getLocalizedString(org.odk.collect.strings.R.string.delete_layer_confirmation_message, layerItem.name)) + .setMessage( + requireActivity().getLocalizedString( + org.odk.collect.strings.R.string.delete_layer_confirmation_message, + layerItem.name + ) + ) .setPositiveButton(org.odk.collect.strings.R.string.delete_layer) { _, _ -> sharedViewModel.deleteLayer(layerItem.id!!) } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt deleted file mode 100644 index 5d4d1521c59..00000000000 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersStateViewModel.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.odk.collect.maps.layers - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.odk.collect.settings.SettingsProvider -import org.odk.collect.settings.keys.ProjectKeys - -class OfflineMapLayersStateViewModel( - private val settingsProvider: SettingsProvider -) : ViewModel() { - private val _checkedLayerId = - MutableLiveData(settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER)) - val checkedLayerId: LiveData = _checkedLayerId - - fun onLayerChecked(layerId: String?) { - _checkedLayerId.value = layerId - } - - fun onLayersChanged(layerIds: List) { - if (!layerIds.contains(_checkedLayerId.value)) { - _checkedLayerId.value = null - settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, null) - } - } - - fun getCheckedLayer(): String? { - return checkedLayerId.value - } -} diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index 1b507b51df5..69b7f1b81e7 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -99,6 +99,10 @@ class OfflineMapLayersViewModel( fun deleteLayer(layerId: String) { _isLoading.value = true scheduler.immediate { + if (settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) == layerId) { + settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, null) + } + referenceLayerRepository.delete(layerId) _existingLayers.postValue(_existingLayers.value?.filter { it.id != layerId }) _isLoading.postValue(false) From c41e31a93bdaf9d5d0a8c493297461f3ebc61f88 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 17 Jun 2024 14:16:16 +0100 Subject: [PATCH 688/750] Move type conversion closer to generic code --- .../formlists/blankformlist/DeleteBlankFormFragment.kt | 6 +++--- .../formlists/savedformlist/DeleteSavedFormFragment.kt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt index 47c3a6cfa37..a56beebaa52 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt @@ -67,7 +67,7 @@ class DeleteBlankFormFragment( this ) { _, result -> val selected = result.getStringArray(MultiSelectControlsFragment.RESULT_SELECTED)!! - onDeleteSelected(selected) + onDeleteSelected(selected.map { it.toLong() }.toLongArray()) } } @@ -85,7 +85,7 @@ class DeleteBlankFormFragment( menuHost.addMenuProvider(blankFormListMenuProvider, viewLifecycleOwner, State.RESUMED) } - private fun onDeleteSelected(selected: Array) { + private fun onDeleteSelected(selected: LongArray) { MaterialAlertDialogBuilder(requireContext()) .setTitle(string.delete_file) .setMessage( @@ -95,7 +95,7 @@ class DeleteBlankFormFragment( ) ) .setPositiveButton(getString(string.delete_yes)) { _, _ -> - blankFormListViewModel.deleteForms(*(selected.map { it.toLong() }.toLongArray())) + blankFormListViewModel.deleteForms(*selected) multiSelectViewModel.unselectAll() } .setNegativeButton(getString(string.delete_no), null) diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt index 86a07d7d532..ee119721afb 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt @@ -70,7 +70,7 @@ class DeleteSavedFormFragment( this ) { _, result -> val selected = result.getStringArray(MultiSelectControlsFragment.RESULT_SELECTED)!! - onDeleteSelected(selected) + onDeleteSelected(selected.map { it.toLong() }.toLongArray()) } } @@ -104,7 +104,7 @@ class DeleteSavedFormFragment( } } - private fun onDeleteSelected(selected: Array) { + private fun onDeleteSelected(selected: LongArray) { MaterialAlertDialogBuilder(requireContext()) .setTitle(string.delete_file) .setMessage( @@ -117,7 +117,7 @@ class DeleteSavedFormFragment( logDelete(selected.size) multiSelectViewModel.unselectAll() - savedFormListViewModel.deleteForms(selected.map { it.toLong() }.toLongArray()).observe( + savedFormListViewModel.deleteForms(selected).observe( viewLifecycleOwner, object : SnackbarPresenterObserver(requireView()) { override fun getSnackbarDetails(value: Int): SnackbarUtils.SnackbarDetails { From a91a5c0904530f277e21ededb45250026369aeba Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 17 Jun 2024 14:19:41 +0100 Subject: [PATCH 689/750] Remove unused field --- .../java/org/odk/collect/lists/selects/SingleSelectViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lists/src/main/java/org/odk/collect/lists/selects/SingleSelectViewModel.kt b/lists/src/main/java/org/odk/collect/lists/selects/SingleSelectViewModel.kt index 677f422812e..535b1aa9dd4 100644 --- a/lists/src/main/java/org/odk/collect/lists/selects/SingleSelectViewModel.kt +++ b/lists/src/main/java/org/odk/collect/lists/selects/SingleSelectViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.map import org.odk.collect.androidshared.livedata.LiveDataUtils -class SingleSelectViewModel(private val data: LiveData>>) : ViewModel() { +class SingleSelectViewModel(data: LiveData>>) : ViewModel() { private val _selected = MutableLiveData(null) private val selected = LiveDataUtils.zip(_selected, data).map { (selected, data) -> From 48a4b12a6c8d19ab51e0be6e3b5966ca72bb914a Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 17 Jun 2024 14:31:36 +0100 Subject: [PATCH 690/750] Use helper factory for ViewModel --- .../org/odk/collect/lists/selects/MultiSelectViewModel.kt | 3 ++- .../org/odk/collect/maps/layers/OfflineMapLayersPicker.kt | 5 +---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lists/src/main/java/org/odk/collect/lists/selects/MultiSelectViewModel.kt b/lists/src/main/java/org/odk/collect/lists/selects/MultiSelectViewModel.kt index 57f284d2127..106f1c1d69d 100644 --- a/lists/src/main/java/org/odk/collect/lists/selects/MultiSelectViewModel.kt +++ b/lists/src/main/java/org/odk/collect/lists/selects/MultiSelectViewModel.kt @@ -65,7 +65,8 @@ class MultiSelectViewModel( selected.value = new } - class Factory(private val data: LiveData>>) : ViewModelProvider.Factory { + class Factory(private val data: LiveData>> = MutableLiveData(emptyList())) : + ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class, extras: CreationExtras): VM { return MultiSelectViewModel(data) as VM diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 0a51a77e746..3ae5df46827 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -50,11 +50,8 @@ class OfflineMapLayersPicker( } } } - private val expandedStateViewModel: MultiSelectViewModel<*> by viewModels { - viewModelFactory { - addInitializer(MultiSelectViewModel::class) { MultiSelectViewModel() } - } + MultiSelectViewModel.Factory() } private val checkedStateViewModel: SingleSelectViewModel by viewModels { From c46d42df4e3192f7aed4c8d122a87a78aa880353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Mon, 24 Jun 2024 11:52:55 -0700 Subject: [PATCH 691/750] Clarify delete confirmation message translation text --- strings/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 1bbeef9ba40..1723400bc82 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -513,8 +513,8 @@ Current project only - - Are you sure you want to delete %1$s offline layer? + + Are you sure you want to delete the %1$s offline layer? Record a point Accuracy: %1$s m From 3ac2ce24326da2f36a89db3ce2f80f6b63d3436f Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 25 Jun 2024 09:43:42 +0100 Subject: [PATCH 692/750] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 853274b9e9c..a1131854ebd 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Releases are planned to happen every 2-3 months (resulting in ~4 releases a year 1. Moving issues not finished in the last release and new items from the [ODK Roadmap](https://getodk.org/roadmap) to the [planning board](https://github.com/orgs/getodk/projects/9/views/25) 2. Giving the core team a few days to review and reflect on the planning board -3. The core team will then meet to trim work not that will not be included in the next release and pitch alternative things to work on +3. The core team will then meet to trim work that will not be included in the next release and pitch alternative things to work on 4. The milestone for the new release is added to [the backlog](https://github.com/orgs/getodk/projects/9) and is prioritized Sometimes issues will be assigned to core team members before they are actually started (moved to "in progress") to make it clear who's going to be working on what. From 38a5c344069326137488c618d85353a8c8495086 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 25 Jun 2024 09:50:26 +0100 Subject: [PATCH 693/750] Correct typos --- docs/STATE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/STATE.md b/docs/STATE.md index 91a837fb62d..dd1c3d99fb9 100644 --- a/docs/STATE.md +++ b/docs/STATE.md @@ -19,7 +19,7 @@ The purpose of this document is to give anyone who reads it a quick overview of * UI is "iconic" (old) but with a lot of inconsistencies and quirks and is best adapted to small screens (although often used on tablets) * A lot of code lives in between one "god" Activity (`FormFillingActivity`) and a process singleton (`FormController`) * Core form entry flow uses custom side-to-side swipe view (in `FormFillingActivity` made up of `ODKView`) -* Questions are rendered using a view "framework" of implementations inheriting from `QuestionWidget` (which is documented at in [WIDGETS.MD](WIDGETS.md)) which are also used to store UI state during form entry +* Questions are rendered using a view "framework" of implementations inheriting from `QuestionWidget` (which is documented in [WIDGETS.MD](WIDGETS.md)) which are also used to store UI state during form entry * App mostly stores data in flat files indexed in SQLite * Access to data in SQLite happens through repository objects which deal in data/domain objects (`FormsRepository` and `Form` for example) * Settings UIs for the app use Android's Preferences abstraction @@ -31,7 +31,7 @@ The purpose of this document is to give anyone who reads it a quick overview of * Forms get into the app from two different sources (Open Rosa servers and disk) but the logic for this is disparate and they don't sit behind a common interface * Instances are linked to the forms they are instances of through formid and version. However, the same formid and version combination could represent multiple forms in storage * `SharedPreferences` is wrapped in app's own `Settings` abstraction -* The form hierarchy is rendered using `FormHierarchyActivity` which hasn't been seriously touched (at a code or design) level for a few years +* The form hierarchy is rendered using `FormHierarchyActivity`, which hasn't been seriously touched (at a code or design) level for a few years ## Where we're going @@ -43,5 +43,5 @@ The purpose of this document is to give anyone who reads it a quick overview of * Replacing async work such as `AsyncTask` with `Flow`/`LiveData` + `Scheduler` abstraction * Gradually removing use of `CursorLoader` (all remaining uses are in `CursorLoaderFactory`) * Using AndroidX Test in new local tests and migrating other local tests as we touch them (from classic Robolectric) -* Moving towards a ["data services"](data_services_architecture.pdf) oriented architecture that has emerged over time that uses AndroidX Architecture Components for the core of the UI (Fragment, View, ViewModel etc) +* Moving towards a ["data services"](data_services_architecture.pdf) oriented architecture that has emerged over time that uses AndroidX Architecture Components for the core of the UI (Fragment, View, ViewModel etc.) * Improving the `MapFragment` abstraction so more logic can be shared between the map engines From 0e3b28364cc89c8c2b57267f28446bcfbd4ef9d0 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 25 Jun 2024 11:30:50 +0200 Subject: [PATCH 694/750] Removed redundant attributes --- maps/src/main/res/layout/offline_map_layers_importer.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/maps/src/main/res/layout/offline_map_layers_importer.xml b/maps/src/main/res/layout/offline_map_layers_importer.xml index d37e995468b..e6a4bdfedf5 100644 --- a/maps/src/main/res/layout/offline_map_layers_importer.xml +++ b/maps/src/main/res/layout/offline_map_layers_importer.xml @@ -129,8 +129,7 @@ android:layout_height="wrap_content" android:paddingStart="@dimen/margin_standard" android:text="@string/all_projects_option" - android:textAppearance="?textAppearanceBodyMedium" - android:translationX="-5dp" /> + android:textAppearance="?textAppearanceBodyMedium" /> + android:textAppearance="?textAppearanceBodyMedium" /> From cd01acb656bb00d9721ed892254bbba3f78215a3 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 25 Jun 2024 11:31:59 +0200 Subject: [PATCH 695/750] Make the icon invisible not gone if it is not needed --- .../odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt index 059c4261bdf..aa04c68b790 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt @@ -4,7 +4,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat -import androidx.core.view.isVisible +import androidx.core.view.isInvisible import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -43,7 +43,7 @@ class OfflineMapLayersPickerAdapter( holder.binding.radioButton.setChecked(layer.isChecked) holder.binding.title.text = layer.name holder.binding.path.text = layer.file?.absolutePath - holder.binding.arrow.isVisible = layer.id != null + holder.binding.arrow.isInvisible = layer.id == null if (layer.isExpanded) { holder.binding.arrow.setImageDrawable(ContextCompat.getDrawable(holder.binding.root.context, org.odk.collect.icons.R.drawable.ic_baseline_collapse_24)) From 732d24e95e4d497d66ebde8bf773bc5f15139d5e Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 25 Jun 2024 17:30:06 +0200 Subject: [PATCH 696/750] Added new strings --- strings/src/main/res/values/strings.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 1723400bc82..e75cef341ba 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -500,6 +500,18 @@ Delete layer + + + %d layer can\'t be added + %d layers can\'t be added + + + + The files you selected are not MBTiles. You can only add MBTiles files. + + + Some of the files you selected are not MBTiles. You can only add MBTiles files. + Layers From 50a495a65909f29485e675995b00c1200a47de6a Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 27 Jun 2024 12:11:21 +0200 Subject: [PATCH 697/750] Display a warning when unsuported layers are selected --- .../maps/layers/OfflineMapLayersImporter.kt | 47 ++++++++++++++++++- .../maps/layers/OfflineMapLayersViewModel.kt | 21 +++++++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt index e417aa12e23..20e7d92d0ae 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt @@ -8,10 +8,14 @@ import androidx.appcompat.widget.Toolbar import androidx.fragment.app.activityViewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.odk.collect.async.Scheduler import org.odk.collect.maps.databinding.OfflineMapLayersImporterBinding import org.odk.collect.material.MaterialFullScreenDialogFragment import org.odk.collect.settings.SettingsProvider +import org.odk.collect.strings.R +import org.odk.collect.strings.localization.getLocalizedQuantityString +import org.odk.collect.strings.localization.getLocalizedString class OfflineMapLayersImporter( private val referenceLayerRepository: ReferenceLayerRepository, @@ -65,8 +69,19 @@ class OfflineMapLayersImporter( } viewModel.layersToImport.observe(this) { layersToImport -> - val adapter = OfflineMapLayersImporterAdapter(layersToImport) + val adapter = OfflineMapLayersImporterAdapter(layersToImport.value.layers) binding.layers.setAdapter(adapter) + + if (!layersToImport.isConsumed()) { + layersToImport.consume() + + if (layersToImport.value.numberOfSelectedLayers == layersToImport.value.numberOfUnsupportedLayers) { + dismiss() + showNoSupportedLayersWarning(layersToImport.value.numberOfUnsupportedLayers) + } else if (layersToImport.value.numberOfUnsupportedLayers > 0) { + showSomeUnsupportedLayersWarning(layersToImport.value.numberOfSelectedLayers - layersToImport.value.numberOfUnsupportedLayers) + } + } } } @@ -79,4 +94,34 @@ class OfflineMapLayersImporter( override fun getToolbar(): Toolbar { return OfflineMapLayersImporterBinding.bind(requireView()).toolbar } + + private fun showNoSupportedLayersWarning(numberOfLayers: Int) { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle( + requireActivity().getLocalizedQuantityString( + R.plurals.non_mbtiles_files_selected_title, + numberOfLayers, + numberOfLayers + ) + ) + .setMessage(requireActivity().getLocalizedString(R.string.all_non_mbtiles_files_selected_message)) + .setPositiveButton(R.string.ok, null) + .create() + .show() + } + + private fun showSomeUnsupportedLayersWarning(numberOfLayers: Int) { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle( + requireActivity().getLocalizedQuantityString( + R.plurals.non_mbtiles_files_selected_title, + numberOfLayers, + numberOfLayers + ) + ) + .setMessage(requireActivity().getLocalizedString(R.string.some_non_mbtiles_files_selected_message)) + .setPositiveButton(R.string.ok, null) + .create() + .show() + } } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index 69b7f1b81e7..1842dbe325d 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.odk.collect.analytics.Analytics +import org.odk.collect.androidshared.data.Consumable import org.odk.collect.androidshared.system.copyToFile import org.odk.collect.androidshared.system.getFileExtension import org.odk.collect.androidshared.system.getFileName @@ -27,8 +28,8 @@ class OfflineMapLayersViewModel( private val _existingLayers = MutableLiveData>() val existingLayers: LiveData> = _existingLayers - private val _layersToImport = MutableLiveData>() - val layersToImport: LiveData> = _layersToImport + private val _layersToImport = MutableLiveData>() + val layersToImport: LiveData> = _layersToImport private lateinit var tempLayersDir: File @@ -67,7 +68,15 @@ class OfflineMapLayersViewModel( } } _isLoading.postValue(false) - _layersToImport.postValue(layers.sortedBy { it.name }) + _layersToImport.postValue( + Consumable( + LayersToImport( + uris.size, + uris.size - layers.size, + layers.sortedBy { it.name } + ) + ) + ) }, foreground = { } ) @@ -119,4 +128,10 @@ class OfflineMapLayersViewModel( Analytics.log(event) } + + data class LayersToImport( + val numberOfSelectedLayers: Int, + val numberOfUnsupportedLayers: Int, + val layers: List + ) } From eccd18dab56ac046d21e6cbb66e875cedce1c24a Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 27 Jun 2024 12:11:29 +0200 Subject: [PATCH 698/750] Added tests --- .../layers/OfflineMapLayersImporterTest.kt | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt index f3f0cbb5ccd..6e95520acb7 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt @@ -1,9 +1,12 @@ package org.odk.collect.maps.layers +import android.app.Application import androidx.core.net.toUri import androidx.fragment.app.testing.FragmentScenario +import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isEnabled @@ -25,6 +28,7 @@ import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule import org.odk.collect.settings.InMemSettingsProvider import org.odk.collect.shared.TempFiles import org.odk.collect.strings.R +import org.odk.collect.strings.localization.getLocalizedQuantityString import org.odk.collect.testshared.FakeScheduler import org.odk.collect.testshared.Interactions import org.odk.collect.testshared.RecyclerViewMatcher @@ -240,6 +244,43 @@ class OfflineMapLayersImporterTest { assertThat(booleanCaptor.secondValue, equalTo(false)) } + @Test + fun `the warning dialog is displayed if some selected files are not supported and the importer dialog is kept displayed`() { + val file1 = TempFiles.createTempFile("layerA", ".txt") + val file2 = TempFiles.createTempFile("layerB", MbtilesFile.FILE_EXTENSION) + + launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext()) + + scheduler.flush() + + val context = ApplicationProvider.getApplicationContext() + onView(withText(context.getLocalizedQuantityString(R.plurals.non_mbtiles_files_selected_title, 1, 1))).inRoot(isDialog()).check(matches(isDisplayed())) + onView(withText(R.string.some_non_mbtiles_files_selected_message)).inRoot(isDialog()).check(matches(isDisplayed())) + onView(withText(R.string.ok)).inRoot(isDialog()).check(matches(isDisplayed())) + + assertThat(it.isVisible, equalTo(true)) + } + } + + @Test + fun `the warning dialog is displayed if all selected files are not supported and the importer dialog is dismissed`() { + val file = TempFiles.createTempFile("layerA", ".txt") + + launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file.toUri()), it.requireContext()) + + scheduler.flush() + + val context = ApplicationProvider.getApplicationContext() + onView(withText(context.getLocalizedQuantityString(R.plurals.non_mbtiles_files_selected_title, 1, 1))).inRoot(isDialog()).check(matches(isDisplayed())) + onView(withText(R.string.all_non_mbtiles_files_selected_message)).inRoot(isDialog()).check(matches(isDisplayed())) + onView(withText(R.string.ok)).inRoot(isDialog()).check(matches(isDisplayed())) + + assertThat(it.isVisible, equalTo(false)) + } + } + private fun launchFragment(): FragmentScenario { return fragmentScenarioLauncherRule.launchInContainer(OfflineMapLayersImporter::class.java) } From f43ad6961bf5932f2b4b0ea23ca244847feb84ad Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 27 Jun 2024 08:55:41 +0200 Subject: [PATCH 699/750] Updated TrackableWorker to count jobs --- .../odk/collect/androidshared/async/TrackableWorker.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/async/TrackableWorker.kt b/androidshared/src/main/java/org/odk/collect/androidshared/async/TrackableWorker.kt index b6ad2e521d0..6bf67213303 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/async/TrackableWorker.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/async/TrackableWorker.kt @@ -3,6 +3,7 @@ package org.odk.collect.androidshared.async import org.odk.collect.androidshared.livedata.MutableNonNullLiveData import org.odk.collect.androidshared.livedata.NonNullLiveData import org.odk.collect.async.Scheduler +import java.util.concurrent.atomic.AtomicInteger import java.util.function.Consumer import java.util.function.Supplier @@ -11,10 +12,15 @@ class TrackableWorker(private val scheduler: Scheduler) { private val _isWorking = MutableNonNullLiveData(false) val isWorking: NonNullLiveData = _isWorking + private var counter = AtomicInteger(0) + fun immediate(background: Supplier, foreground: Consumer) { + counter.incrementAndGet() _isWorking.value = true scheduler.immediate(background) { result -> - _isWorking.value = false + if (counter.decrementAndGet() == 0) { + _isWorking.value = false + } foreground.accept(result) } } From 94b2a7ca87561f4301e5a58302ec65b0f95b01ea Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 27 Jun 2024 08:58:27 +0200 Subject: [PATCH 700/750] Added tests --- .../async/TrackableWorkerTest.kt | 28 +++++++++++++++++++ .../odk/collect/testshared/FakeScheduler.kt | 12 ++++++++ 2 files changed, 40 insertions(+) create mode 100644 androidshared/src/test/java/org/odk/collect/androidshared/async/TrackableWorkerTest.kt diff --git a/androidshared/src/test/java/org/odk/collect/androidshared/async/TrackableWorkerTest.kt b/androidshared/src/test/java/org/odk/collect/androidshared/async/TrackableWorkerTest.kt new file mode 100644 index 00000000000..4a66f7fc614 --- /dev/null +++ b/androidshared/src/test/java/org/odk/collect/androidshared/async/TrackableWorkerTest.kt @@ -0,0 +1,28 @@ +package org.odk.collect.androidshared.async + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.odk.collect.testshared.FakeScheduler + +@RunWith(AndroidJUnit4::class) +class TrackableWorkerTest { + private val scheduler = FakeScheduler() + private val trackableWorker = TrackableWorker(scheduler) + + @Test + fun `TrackableWorker counts work in progress`() { + trackableWorker.immediate {} + trackableWorker.immediate {} + + scheduler.runFirstBackground() + scheduler.runFirstForeground() + assertThat(trackableWorker.isWorking.value, equalTo(true)) + + scheduler.runFirstBackground() + scheduler.runFirstForeground() + assertThat(trackableWorker.isWorking.value, equalTo(false)) + } +} diff --git a/test-shared/src/main/java/org/odk/collect/testshared/FakeScheduler.kt b/test-shared/src/main/java/org/odk/collect/testshared/FakeScheduler.kt index b4a3f153a03..3646e5f2223 100644 --- a/test-shared/src/main/java/org/odk/collect/testshared/FakeScheduler.kt +++ b/test-shared/src/main/java/org/odk/collect/testshared/FakeScheduler.kt @@ -77,6 +77,12 @@ class FakeScheduler : Scheduler { return flow.flowOn(backgroundDispatcher) } + fun runFirstForeground() { + if (foregroundTasks.isNotEmpty()) { + foregroundTasks.removeFirst().run() + } + } + fun runForeground() { while (foregroundTasks.isNotEmpty()) { foregroundTasks.remove().run() @@ -103,6 +109,12 @@ class FakeScheduler : Scheduler { } } + fun runFirstBackground() { + if (backgroundTasks.isNotEmpty()) { + backgroundTasks.removeFirst().run() + } + } + fun runBackground() { while (backgroundTasks.isNotEmpty()) { backgroundTasks.remove().run() From fd6a5e3c54c8a36f4f03b6c86a144cc0f56d566b Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 27 Jun 2024 08:59:09 +0200 Subject: [PATCH 701/750] Naming improvements --- .../org/odk/collect/androidshared/async/TrackableWorker.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/async/TrackableWorker.kt b/androidshared/src/main/java/org/odk/collect/androidshared/async/TrackableWorker.kt index 6bf67213303..2360b5fd63c 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/async/TrackableWorker.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/async/TrackableWorker.kt @@ -12,13 +12,13 @@ class TrackableWorker(private val scheduler: Scheduler) { private val _isWorking = MutableNonNullLiveData(false) val isWorking: NonNullLiveData = _isWorking - private var counter = AtomicInteger(0) + private var activeBackgroundJobsCounter = AtomicInteger(0) fun immediate(background: Supplier, foreground: Consumer) { - counter.incrementAndGet() + activeBackgroundJobsCounter.incrementAndGet() _isWorking.value = true scheduler.immediate(background) { result -> - if (counter.decrementAndGet() == 0) { + if (activeBackgroundJobsCounter.decrementAndGet() == 0) { _isWorking.value = false } foreground.accept(result) From 83335af55581e50a2e14e19bcce73d7523c3ebd9 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 27 Jun 2024 09:06:42 +0200 Subject: [PATCH 702/750] Use TrackableWorker in OfflineMapLayersViewModel --- .../maps/layers/OfflineMapLayersImporter.kt | 2 +- .../maps/layers/OfflineMapLayersPicker.kt | 2 +- .../maps/layers/OfflineMapLayersViewModel.kt | 28 ++++++------------- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt index 20e7d92d0ae..ea8b68a0d6f 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt @@ -56,7 +56,7 @@ class OfflineMapLayersImporter( super.onViewCreated(view, savedInstanceState) val binding = OfflineMapLayersImporterBinding.bind(view) - viewModel.isLoading.observe(this) { isLoading -> + viewModel.trackableWorker.isWorking.observe(this) { isLoading -> if (isLoading) { binding.addLayerButton.isEnabled = false binding.layers.visibility = View.GONE diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 3ae5df46827..6c04efa9963 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -124,7 +124,7 @@ class OfflineMapLayersPicker( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val binding = OfflineMapLayersPickerBinding.bind(view) - sharedViewModel.isLoading.observe(this) { isLoading -> + sharedViewModel.trackableWorker.isWorking.observe(this) { isLoading -> if (isLoading) { binding.progressIndicator.visibility = View.VISIBLE binding.layers.visibility = View.GONE diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index 1842dbe325d..e0ed74994a6 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.odk.collect.analytics.Analytics +import org.odk.collect.androidshared.async.TrackableWorker import org.odk.collect.androidshared.data.Consumable import org.odk.collect.androidshared.system.copyToFile import org.odk.collect.androidshared.system.getFileExtension @@ -19,11 +20,10 @@ import java.io.File class OfflineMapLayersViewModel( private val referenceLayerRepository: ReferenceLayerRepository, - private val scheduler: Scheduler, + scheduler: Scheduler, private val settingsProvider: SettingsProvider ) : ViewModel() { - private val _isLoading = MutableLiveData() - val isLoading: LiveData = _isLoading + val trackableWorker = TrackableWorker(scheduler) private val _existingLayers = MutableLiveData>() val existingLayers: LiveData> = _existingLayers @@ -38,20 +38,16 @@ class OfflineMapLayersViewModel( } private fun loadExistingLayers() { - _isLoading.value = true - scheduler.immediate( + trackableWorker.immediate( background = { val layers = referenceLayerRepository.getAll().sortedBy { it.name } - _isLoading.postValue(false) _existingLayers.postValue(layers) - }, - foreground = { } + } ) } fun loadLayersToImport(uris: List, context: Context) { - _isLoading.value = true - scheduler.immediate( + trackableWorker.immediate( background = { tempLayersDir = TempFiles.createTempDir().also { it.deleteOnExit() @@ -67,7 +63,6 @@ class OfflineMapLayersViewModel( } } } - _isLoading.postValue(false) _layersToImport.postValue( Consumable( LayersToImport( @@ -77,14 +72,12 @@ class OfflineMapLayersViewModel( ) ) ) - }, - foreground = { } + } ) } fun importNewLayers(shared: Boolean) { - _isLoading.value = true - scheduler.immediate( + trackableWorker.immediate( background = { val layers = tempLayersDir.listFiles() logImport(layers) @@ -93,7 +86,6 @@ class OfflineMapLayersViewModel( referenceLayerRepository.addLayer(it, shared) } tempLayersDir.delete() - _isLoading.postValue(false) }, foreground = { loadExistingLayers() @@ -106,15 +98,13 @@ class OfflineMapLayersViewModel( } fun deleteLayer(layerId: String) { - _isLoading.value = true - scheduler.immediate { + trackableWorker.immediate { if (settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) == layerId) { settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, null) } referenceLayerRepository.delete(layerId) _existingLayers.postValue(_existingLayers.value?.filter { it.id != layerId }) - _isLoading.postValue(false) } } From 0317163991bfb28c18f455bb0840b0dd55f9abfe Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 27 Jun 2024 17:02:23 +0200 Subject: [PATCH 703/750] Added tests --- .../layers/OfflineMapLayersImporterTest.kt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt index 6e95520acb7..cf027a1f6ec 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt @@ -281,6 +281,53 @@ class OfflineMapLayersImporterTest { } } + @Test + fun `the warning dialog shows correct number of unsupported layers`() { + val context = ApplicationProvider.getApplicationContext() + + launchFragment().onFragment { + // Three unsupported layers + it.viewModel.loadLayersToImport( + listOf( + TempFiles.createTempFile("layerA", ".txt").toUri(), + TempFiles.createTempFile("layerB", ".txt").toUri(), + TempFiles.createTempFile("layerC", ".txt").toUri() + ), + context + ) + scheduler.flush() + onView(withText(context.getLocalizedQuantityString(R.plurals.non_mbtiles_files_selected_title, 3, 3))).inRoot(isDialog()).check(matches(isDisplayed())) + } + + launchFragment().onFragment { + // Two unsupported layers + it.viewModel.loadLayersToImport( + listOf( + TempFiles.createTempFile("layerA", ".txt").toUri(), + TempFiles.createTempFile("layerB", ".txt").toUri(), + TempFiles.createTempFile("layerC", MbtilesFile.FILE_EXTENSION).toUri() + ), + context + ) + scheduler.flush() + onView(withText(context.getLocalizedQuantityString(R.plurals.non_mbtiles_files_selected_title, 2, 2))).inRoot(isDialog()).check(matches(isDisplayed())) + } + + launchFragment().onFragment { + // One unsupported layer + it.viewModel.loadLayersToImport( + listOf( + TempFiles.createTempFile("layerA", ".txt").toUri(), + TempFiles.createTempFile("layerB", MbtilesFile.FILE_EXTENSION).toUri(), + TempFiles.createTempFile("layerC", MbtilesFile.FILE_EXTENSION).toUri() + ), + context + ) + scheduler.flush() + onView(withText(context.getLocalizedQuantityString(R.plurals.non_mbtiles_files_selected_title, 1, 1))).inRoot(isDialog()).check(matches(isDisplayed())) + } + } + private fun launchFragment(): FragmentScenario { return fragmentScenarioLauncherRule.launchInContainer(OfflineMapLayersImporter::class.java) } From 7ecac321a53f83e21f1039e2c729f97b8eb24569 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 27 Jun 2024 17:02:54 +0200 Subject: [PATCH 704/750] Fixed displaying the number of unsupported layers --- .../org/odk/collect/maps/layers/OfflineMapLayersImporter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt index 20e7d92d0ae..d9f77d596d6 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt @@ -79,7 +79,7 @@ class OfflineMapLayersImporter( dismiss() showNoSupportedLayersWarning(layersToImport.value.numberOfUnsupportedLayers) } else if (layersToImport.value.numberOfUnsupportedLayers > 0) { - showSomeUnsupportedLayersWarning(layersToImport.value.numberOfSelectedLayers - layersToImport.value.numberOfUnsupportedLayers) + showSomeUnsupportedLayersWarning(layersToImport.value.numberOfUnsupportedLayers) } } } From 56ad6f08663ec86a7c55e594504aa9af8b36b1a6 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 28 Jun 2024 12:03:14 +0200 Subject: [PATCH 705/750] Fixed SingleSelectViewModel --- .../org/odk/collect/lists/selects/SelectItem.kt | 2 +- .../lists/selects/SingleSelectViewModel.kt | 13 ++++++------- .../maps/layers/OfflineMapLayersPicker.kt | 16 +++++++--------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/lists/src/main/java/org/odk/collect/lists/selects/SelectItem.kt b/lists/src/main/java/org/odk/collect/lists/selects/SelectItem.kt index 75dc3b7a573..e3b0fc27825 100644 --- a/lists/src/main/java/org/odk/collect/lists/selects/SelectItem.kt +++ b/lists/src/main/java/org/odk/collect/lists/selects/SelectItem.kt @@ -1,3 +1,3 @@ package org.odk.collect.lists.selects -data class SelectItem(val id: String, val item: T, val selected: Boolean = false) +data class SelectItem(val id: String, val item: T) diff --git a/lists/src/main/java/org/odk/collect/lists/selects/SingleSelectViewModel.kt b/lists/src/main/java/org/odk/collect/lists/selects/SingleSelectViewModel.kt index 535b1aa9dd4..e9f4cec7bae 100644 --- a/lists/src/main/java/org/odk/collect/lists/selects/SingleSelectViewModel.kt +++ b/lists/src/main/java/org/odk/collect/lists/selects/SingleSelectViewModel.kt @@ -6,15 +6,14 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.map import org.odk.collect.androidshared.livedata.LiveDataUtils -class SingleSelectViewModel(data: LiveData>>) : ViewModel() { +class SingleSelectViewModel( + selected: String?, + data: LiveData>> +) : ViewModel() { - private val _selected = MutableLiveData(null) + private val _selected = MutableLiveData(selected) private val selected = LiveDataUtils.zip(_selected, data).map { (selected, data) -> - if (selected != null && data.any { it.id == selected }) { - selected - } else { - data.find { it.selected }?.id - } + selected.takeIf { id -> data.any { it.id == id } } } fun getSelected(): LiveData { diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 3ae5df46827..0925d2d967e 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -57,16 +57,14 @@ class OfflineMapLayersPicker( private val checkedStateViewModel: SingleSelectViewModel by viewModels { viewModelFactory { addInitializer(SingleSelectViewModel::class) { - SingleSelectViewModel(sharedViewModel.existingLayers.map { - it.map { layer -> - SelectItem( - layer.id, - layer, - settingsProvider.getUnprotectedSettings() - .getString(ProjectKeys.KEY_REFERENCE_LAYER) == layer.id - ) + SingleSelectViewModel( + settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER), + sharedViewModel.existingLayers.map { + it.map { layer -> + SelectItem(layer.id, layer) + } } - }) + ) } } } From 1b169ce62a53c9d033de308bc261749c8e73a9c1 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 28 Jun 2024 12:03:46 +0200 Subject: [PATCH 706/750] Added tests --- .../selects/SingleSelectViewModelTest.kt | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 lists/src/test/java/org/odk/collect/lists/selects/SingleSelectViewModelTest.kt diff --git a/lists/src/test/java/org/odk/collect/lists/selects/SingleSelectViewModelTest.kt b/lists/src/test/java/org/odk/collect/lists/selects/SingleSelectViewModelTest.kt new file mode 100644 index 00000000000..30c8ed4c8c2 --- /dev/null +++ b/lists/src/test/java/org/odk/collect/lists/selects/SingleSelectViewModelTest.kt @@ -0,0 +1,68 @@ +package org.odk.collect.lists.selects + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.Rule +import org.junit.Test +import org.odk.collect.androidtest.getOrAwaitValue + +class SingleSelectViewModelTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Test + fun `nothing is selected on viewmodel initialization if selected item id is null`() { + val selected = null + val data: LiveData>> = MutableLiveData(listOf(SelectItem("1", 1), SelectItem("2", 2))) + val viewModel = SingleSelectViewModel(selected, data) + + assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(null)) + } + + @Test + fun `nothing is selected on viewmodel initialization if selected item id is not null but there is no item witch matching id`() { + val selected = "0" + val data: LiveData>> = MutableLiveData(listOf(SelectItem("1", 1), SelectItem("2", 2))) + val viewModel = SingleSelectViewModel(selected, data) + + assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(null)) + } + + @Test + fun `proper item is selected on viewmodel initialization if selected item id is not null`() { + val selected = "1" + val data: LiveData>> = MutableLiveData(listOf(SelectItem("1", 1), SelectItem("2", 2))) + val viewModel = SingleSelectViewModel(selected, data) + + assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo("1")) + } + + @Test + fun `getSelected returns selected item id`() { + val selected = null + val data: LiveData>> = MutableLiveData(listOf(SelectItem("1", 1), SelectItem("2", 2))) + val viewModel = SingleSelectViewModel(selected, data) + + viewModel.select("1") + assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo("1")) + + viewModel.select("2") + assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo("2")) + } + + @Test + fun `clear unselects selected item`() { + val selected = null + val data: LiveData>> = MutableLiveData(listOf(SelectItem("1", 1), SelectItem("2", 2))) + val viewModel = SingleSelectViewModel(selected, data) + + viewModel.select("1") + assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo("1")) + viewModel.clear() + assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(null)) + } +} From 0d31adc1dfee6d25d07361bbc4a2ab8739a0211c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Fri, 28 Jun 2024 10:21:21 -0700 Subject: [PATCH 707/750] Update translations for beta --- strings/src/main/res/values-af/strings.xml | 19 +- strings/src/main/res/values-am/strings.xml | 18 +- strings/src/main/res/values-ar/strings.xml | 19 +- strings/src/main/res/values-bg/strings.xml | 18 +- strings/src/main/res/values-bn/strings.xml | 19 +- strings/src/main/res/values-ca/strings.xml | 19 +- strings/src/main/res/values-cs/strings.xml | 19 +- strings/src/main/res/values-da/strings.xml | 19 +- strings/src/main/res/values-de/strings.xml | 48 +- strings/src/main/res/values-es/strings.xml | 19 +- strings/src/main/res/values-et/strings.xml | 18 +- .../src/main/res/values-fa-rAF/strings.xml | 19 +- strings/src/main/res/values-fa/strings.xml | 19 +- strings/src/main/res/values-fi/strings.xml | 46 +- strings/src/main/res/values-fr/strings.xml | 49 +- strings/src/main/res/values-hi/strings.xml | 18 +- strings/src/main/res/values-ht/strings.xml | 18 +- strings/src/main/res/values-in/strings.xml | 19 +- strings/src/main/res/values-it/strings.xml | 26 +- strings/src/main/res/values-ja/strings.xml | 19 +- strings/src/main/res/values-ka/strings.xml | 20 +- strings/src/main/res/values-km/strings.xml | 18 +- strings/src/main/res/values-ln/strings.xml | 18 +- .../src/main/res/values-lo-rLA/strings.xml | 19 +- strings/src/main/res/values-lt/strings.xml | 20 +- strings/src/main/res/values-mg/strings.xml | 18 +- strings/src/main/res/values-ml/strings.xml | 18 +- strings/src/main/res/values-mr/strings.xml | 18 +- strings/src/main/res/values-ms/strings.xml | 18 +- strings/src/main/res/values-my/strings.xml | 19 +- .../src/main/res/values-ne-rNP/strings.xml | 19 +- strings/src/main/res/values-nl/strings.xml | 18 +- strings/src/main/res/values-no/strings.xml | 20 +- strings/src/main/res/values-pl/strings.xml | 19 +- strings/src/main/res/values-ps/strings.xml | 18 +- strings/src/main/res/values-pt/strings.xml | 31 +- strings/src/main/res/values-ro/strings.xml | 19 +- strings/src/main/res/values-ru/strings.xml | 19 +- strings/src/main/res/values-rw/strings.xml | 19 +- strings/src/main/res/values-si/strings.xml | 18 +- strings/src/main/res/values-sl/strings.xml | 19 +- strings/src/main/res/values-so/strings.xml | 18 +- strings/src/main/res/values-sq/strings.xml | 18 +- strings/src/main/res/values-sr/strings.xml | 18 +- .../src/main/res/values-sv-rSE/strings.xml | 19 +- .../src/main/res/values-sw-rKE/strings.xml | 19 +- strings/src/main/res/values-sw/strings.xml | 19 +- strings/src/main/res/values-te/strings.xml | 19 +- .../src/main/res/values-th-rTH/strings.xml | 18 +- strings/src/main/res/values-ti/strings.xml | 18 +- strings/src/main/res/values-tl/strings.xml | 20 +- strings/src/main/res/values-tr/strings.xml | 18 +- strings/src/main/res/values-uk/strings.xml | 18 +- strings/src/main/res/values-ur/strings.xml | 19 +- strings/src/main/res/values-vi/strings.xml | 20 +- .../src/main/res/values-zh-rTW/strings.xml | 1087 +++++++++++++++++ strings/src/main/res/values-zh/strings.xml | 145 ++- strings/src/main/res/values-zu/strings.xml | 19 +- 58 files changed, 1936 insertions(+), 449 deletions(-) create mode 100644 strings/src/main/res/values-zh-rTW/strings.xml diff --git a/strings/src/main/res/values-af/strings.xml b/strings/src/main/res/values-af/strings.xml index ca22cb6634d..f59145e97aa 100644 --- a/strings/src/main/res/values-af/strings.xml +++ b/strings/src/main/res/values-af/strings.xml @@ -180,17 +180,21 @@ - - - - - - - + + + + + + + + + + + Jammer, Posisie-verskaffers is afgeskakel! @@ -341,6 +345,7 @@ Voer Admin wagwoord in Deselekteer om van Hoofkieslys af te haal Reset + Reset - - - - - - + + + + + + + + + + + ይቅርታ፣ የአካባቢ አቅራቢዎች አይሰሩም፡፡ diff --git a/strings/src/main/res/values-ar/strings.xml b/strings/src/main/res/values-ar/strings.xml index 7b887314850..02a0b2b30aa 100644 --- a/strings/src/main/res/values-ar/strings.xml +++ b/strings/src/main/res/values-ar/strings.xml @@ -380,18 +380,21 @@ غير قادر على الوصول إلى خرائط الجوجل. هل خدمات Google Play مثبتة؟ بوزيترون المادة المظلمة - - الطبقة المرجعية - - - - - - + + + + + + + + + + + سجل نقطة الدقة: %1$s متر مزود خدمة تحديد الموقع: %s diff --git a/strings/src/main/res/values-bg/strings.xml b/strings/src/main/res/values-bg/strings.xml index 86671647647..1b0369cc4e4 100644 --- a/strings/src/main/res/values-bg/strings.xml +++ b/strings/src/main/res/values-bg/strings.xml @@ -118,17 +118,21 @@ - - - - - - - + + + + + + + + + + + diff --git a/strings/src/main/res/values-bn/strings.xml b/strings/src/main/res/values-bn/strings.xml index 9d3ec5b3ca5..336c6f1f6e8 100644 --- a/strings/src/main/res/values-bn/strings.xml +++ b/strings/src/main/res/values-bn/strings.xml @@ -151,17 +151,21 @@ ভূখণ্ড হাইব্রিড উপগ্রহ - - - - - - - + + + + + + + + + + + দুঃখিত, অবস্থান প্রদানকারী সেবা বন্ধ আছে! @@ -307,6 +311,7 @@ এডমিন পাসওয়ার্ড দিন। প্রধান মেনু্তে পাবেন রিসেট + রিসেট - - - - - - + + + + + + + + + + + Atenció, la Locatització està desactivada! Latitud: %1$s\nLongitud: %2$s\nAltitud: %3$sm\nPrecisió: %4$sm @@ -304,6 +308,7 @@ # UNPROTECTED SETTINGS ##############################################--> + Servidor diff --git a/strings/src/main/res/values-cs/strings.xml b/strings/src/main/res/values-cs/strings.xml index 8ab7e0f7a75..f6be4c7ff49 100644 --- a/strings/src/main/res/values-cs/strings.xml +++ b/strings/src/main/res/values-cs/strings.xml @@ -385,18 +385,21 @@ Nelze přistupovat k Mapám Google. Je služba Google Play nainstalována? Positron Temná hmota - - Referenční vrstva - - - - - - + + + + + + + + + + + Zaznamenat bod Přesnost: %1$s m Poskytovatel polohy: %s diff --git a/strings/src/main/res/values-da/strings.xml b/strings/src/main/res/values-da/strings.xml index d82eea9261b..c528748dcd0 100644 --- a/strings/src/main/res/values-da/strings.xml +++ b/strings/src/main/res/values-da/strings.xml @@ -222,18 +222,21 @@ Udendørs Topografisk Positron - - Referencelag - - - - - - + + + + + + + + + + + Præcision: 1%1$s m Lokalitetsudbyder: 1%s Geolokation er slået fra diff --git a/strings/src/main/res/values-de/strings.xml b/strings/src/main/res/values-de/strings.xml index 57276f51117..a1e86e4416d 100644 --- a/strings/src/main/res/values-de/strings.xml +++ b/strings/src/main/res/values-de/strings.xml @@ -391,18 +391,34 @@ Zugriff auf Google Maps nicht möglich. Sind die Google Play Services installiert? Positron Dunkle Materie - - Referenzebene - - - - - - + + Offline-Ebenen + + Ebene + + Offline-Ebene auswählen + + Wählen Sie die Offline-Ebene aus, die für alle Karten in diesem Projekt verwendet werden soll. Sie können der Liste Optionen hinzufügen, indem Sie eine MBTiles-Datei von Ihrem Gerät auswählen, und vorhandene Ebenen aus der Liste löschen.\" + + Mehr über das Hinzufügen von MBTiles erfahren? + + Ebenen hinzufügen + + Ebene löschen + + + + Ebenen + Ebenenzugriff auswählen + Sollen die Ebenen in allen Projekten oder nur im aktuellen verfügbar sein? + Alle Projekte + Nur aktuelles Projekt + + Möchten sie wirklich die %1$s Offline-Ebene löschen? Punkt erfassen Genauigkeit: %1$s m Standortdienst: %s @@ -912,11 +928,16 @@ Objekte + Lokale Entitäten aktivieren + Follow-Up-Formulare werden konsistente Objektlisten haben und lokal erzeugte oder aktualisierte Objekte beinhalten. + Objektliste anzeigen Alle löschen + Objektliste hinzufügen + Offline @@ -1041,18 +1062,29 @@ Ihre Arbeit Wiederherstellen? + Collect wurde am \'EEE, MMM dd, yyyy \'um\' HH:mm\' unerwartet beendet und hat ihre Arbeit gespeichert.\n\nWollen Sie die gespeicherte Arbeit wiederherstellen oder verwerfen? Wiederherstellen Verwerfen + Punkt erhalten + Punkt anzeigen oder ändern + Punkt anzeigen + Punkt ändern + Linie erhalten + Linie anzeigen oder ändern + Linie anzeigen + Polygon erhalten + Polygon anzeigen oder ändern + Polygon anzeigen diff --git a/strings/src/main/res/values-es/strings.xml b/strings/src/main/res/values-es/strings.xml index 1358a425726..ff6ebda2a7b 100644 --- a/strings/src/main/res/values-es/strings.xml +++ b/strings/src/main/res/values-es/strings.xml @@ -387,18 +387,21 @@ No se puede acceder a Google Maps. ¿Están los servicios de Google Play instalados? Positrón Materia oscura - - Capa de referencia - - - - - - + + + + + + + + + + + Registrando un punto Precisión: %1$s m Proveedor de Ubicación: %s diff --git a/strings/src/main/res/values-et/strings.xml b/strings/src/main/res/values-et/strings.xml index 8d8e889b805..47c7e83236f 100644 --- a/strings/src/main/res/values-et/strings.xml +++ b/strings/src/main/res/values-et/strings.xml @@ -210,17 +210,21 @@ Hübriid Satelliit Puudub ligipääs Google Maps-ile. Veenduge, et Google Play teenused on paigaldatud. - - - - - - - + + + + + + + + + + + Kahjuks on asukoha küsimine maha keeratud! Laiuskraad: %1$s\nPikkuskraad: %2$s\nKõrgus: %3$sm\nTäpsusaste: %4$sm diff --git a/strings/src/main/res/values-fa-rAF/strings.xml b/strings/src/main/res/values-fa-rAF/strings.xml index fd83acbdeed..b322c456c27 100644 --- a/strings/src/main/res/values-fa-rAF/strings.xml +++ b/strings/src/main/res/values-fa-rAF/strings.xml @@ -357,18 +357,21 @@ دسترسی به نقشه های گوگل امکان پذیر نیست. آیا خدمات Google Play نصب شده است؟ پوزیترون ماده تاریک - - مرجع به لایه - - - - - - + + + + + + + + + + + یک نقطه را ثبت کنید دقت: %1$s متر ارائه دهنده مکان: %s diff --git a/strings/src/main/res/values-fa/strings.xml b/strings/src/main/res/values-fa/strings.xml index fd83acbdeed..b322c456c27 100644 --- a/strings/src/main/res/values-fa/strings.xml +++ b/strings/src/main/res/values-fa/strings.xml @@ -357,18 +357,21 @@ دسترسی به نقشه های گوگل امکان پذیر نیست. آیا خدمات Google Play نصب شده است؟ پوزیترون ماده تاریک - - مرجع به لایه - - - - - - + + + + + + + + + + + یک نقطه را ثبت کنید دقت: %1$s متر ارائه دهنده مکان: %s diff --git a/strings/src/main/res/values-fi/strings.xml b/strings/src/main/res/values-fi/strings.xml index 0738b66ab5c..07a2a395c30 100644 --- a/strings/src/main/res/values-fi/strings.xml +++ b/strings/src/main/res/values-fi/strings.xml @@ -391,29 +391,40 @@ Google Maps -palveluun ei saada yhteyttä. Onko Google Play Services asennettu? Positroni Pimeä aine - - Viitekerros - + + Offlinetasot + Kerros - - Valitse viitekerros - - Valitse viitekerros jota käytetään kaikissa tämän projektin kartoissa. Voit lisätä vaihtoehtoja listaan valitsemalla MBTile-tiedoston laitteeltasi. - + + Valitse offlinetaso + + Valitse offline-taso kaikkiin karttoihin tässä projektissa. Voit lisätä vaihtoehtoja listaan valitsemalla MBTile-tiedoston laitteellasi tai poistaa aikaisemmin lisättyjä tasoja listalta. + Opi lisää MBTile-tiedostojen lisäämisestä. - - Lisää kerros - - Poista kerros + + Lisää tasoja + + Poista taso + + + %d tasoa ei voi lisätä + %d tasoa ei voi lisätä + + + Valitsemasi tiedostot eivät ole MBTiles-tiedostoja. Voit ainoastaan lisätä MBTiles-tiedostoja. + + Jotkut valitsemistasi tiedostoista eivät ole MBTiles-tiedostoja. Voit ainoastaan lisätä MBTiles-tiedostoja. - Kerrokset + Tasot - Valitse kerroksen käyttöoikeudet - Haluatko kerroksen olevan käytettävissä kaikissa projekteissa vai ainoastaan tässä projektissa? + Valitse tasojen saatavuus + Haluatko tasojen olevan käytettävissä kaikissa projekteissa vai ainoastaan nykyisessä? - Kaikissa projekteissa + Kaikissa projekteissa - Vain tässä projektissa + Vain tässä projektissa + + Haluatko varmasti poistaa offline-tason %1$s? Tallenna piste Tarkkuus: %1$s m Sijaintilähde: %s @@ -932,6 +943,7 @@ Lisää entiteettilista + Offline diff --git a/strings/src/main/res/values-fr/strings.xml b/strings/src/main/res/values-fr/strings.xml index 3d84e3b746e..bd6f909cdb2 100644 --- a/strings/src/main/res/values-fr/strings.xml +++ b/strings/src/main/res/values-fr/strings.xml @@ -391,29 +391,41 @@ Impossible d’accéder à Google Maps. Google Play Services est-il installé ? Positron Matière Sombre - - Couche de référence - + + Couches de carte hors ligne + Couche - - Sélectionnez la couche de référence - - Sélectionnez la couche de référence à utiliser pour toutes les cartes de ce projet. Vous pouvez ajouter des options à la liste en sélectionnant un fichier MBTile sur votre appareil. - + + Choisir couche hors ligne + + Choisissez la couche hors ligne à utiliser pour toutes les cartes de ce projet. Vous pouvez ajouter des options à la liste en choisissant un fichier MBTiles de votre appareil. Vous pouvez également supprimer des couches de la liste. + En apprendre plus à propos de l\'ajout de MBTiles. - - Ajouter une couche - - Supprimer une couche + + Ajouter chouches + + Supprimer une couche + + + %d couche n\'a pas pu être ajoutée. + %d couches n\'ont pas pu être ajoutées. + %d couches n\'ont pas pu être ajoutées. + + + Les fichiers que vous avez sélectionnés ne sont pas des fichiers MBTiles. Vous pouvez seulement ajouter des fichiers MBTiles. + + Certains des fichiers que vous avez sélectionnés ne sont pas des fichiers MBTiles. Vous pouvez seulement ajouter des fichiers MBTiles. - Couches + Couches - Sélectionner l\'accès à la couche - Voulez vous que la couche soit disponible dans tous les projets ou seulement dans le projet courant ? + Choisir l\'accès aux couches + Voulez vous que les couches soient disponibles dans tous les projets ou seulement dans le projet courant ? - Tous les projets + Tous les projets - Projet courant seulement + Projet courant seulement + + Voulez-vous vraiment supprimer la couche hors ligne %1$s? Enregistrer un point Précision: %1$s m Fournisseur de la localisation: 1%s @@ -939,7 +951,7 @@ - L\'application a rencontré un problème durant l\'usage précédant! + L\'application a rencontré un problème durant l\'usage précédent! Impossible de démarrer l\'application! Récupérer votre travail ? + \'Collect s\'est arrété de manière inattendue le \'EEE dd MMM yyyy à HH:mm\' et a sauvegardé votre travail.\n\nVoulez vous le récupérer ou l\'ignorer ?\' Récupérer ? diff --git a/strings/src/main/res/values-hi/strings.xml b/strings/src/main/res/values-hi/strings.xml index 493baa1f2c6..9b45e3c6eda 100644 --- a/strings/src/main/res/values-hi/strings.xml +++ b/strings/src/main/res/values-hi/strings.xml @@ -226,17 +226,21 @@ हाइब्रिड उपग्रह गूगल मानचित्र तक पहुंचने में असमर्थ क्या गूगल प्ले सर्विस इनस्टॉल है? - - - - - - - + + + + + + + + + + + क्षमा करें, लोकेशन प्रदाता सक्षम नही हैं! अक्षांश: %1$s \n देशांतर: %2$s \n ऊंचाई: %3$sm \n शुद्धता: %4$sm diff --git a/strings/src/main/res/values-ht/strings.xml b/strings/src/main/res/values-ht/strings.xml index 62ed3fb8fec..ef17e6a4977 100644 --- a/strings/src/main/res/values-ht/strings.xml +++ b/strings/src/main/res/values-ht/strings.xml @@ -204,17 +204,21 @@ - - - - - - - + + + + + + + + + + + Ale nan paramèt diff --git a/strings/src/main/res/values-in/strings.xml b/strings/src/main/res/values-in/strings.xml index 95b9f1e89b5..a11652b8506 100644 --- a/strings/src/main/res/values-in/strings.xml +++ b/strings/src/main/res/values-in/strings.xml @@ -384,18 +384,21 @@ Tidak dapat mengakses Google Maps. Apakah Google Play Services telah terinstal? Positron Dark Matter - - Lapisan referensi - - - - - - + + + + + + + + + + + Rekam titik Tingkat akurasi adalah %1$s meter Lokasi penyedia jasa: %s diff --git a/strings/src/main/res/values-it/strings.xml b/strings/src/main/res/values-it/strings.xml index 6837fae019b..45d3e4cfbad 100644 --- a/strings/src/main/res/values-it/strings.xml +++ b/strings/src/main/res/values-it/strings.xml @@ -175,6 +175,7 @@ L\'applicazione richiesta è mancante. Per piacere inserisci manualmente la lettura. L\'applicativo esterno non ha fornito l\'informazione attesa. + Stampa Inizio Stampa La stampante richiesta non è installata. Per piacere installa la stampante. Apri URL @@ -388,18 +389,22 @@ Impossibile accedere a Google Maps. Il Servizio di Google Play è installato? Posizione Scuro è importante - - Livello di riferimento - - - - - - + + + + + + + + + + + Tutti i progetti + Memorizza un punto Precisione: %1$s m Fornitore di posizione: %s @@ -912,6 +917,7 @@ + Deseleziona tutto + Recuperare il tuo lavoro? @@ -1048,6 +1055,9 @@ + Ottieni il poligono + Vedi o Cambia il poligono + Visualizza poligono diff --git a/strings/src/main/res/values-ja/strings.xml b/strings/src/main/res/values-ja/strings.xml index 376b92f9133..446cb5da6a2 100644 --- a/strings/src/main/res/values-ja/strings.xml +++ b/strings/src/main/res/values-ja/strings.xml @@ -350,18 +350,21 @@ Google マップにアクセスできません。 Google Play サービスはインストールされていますか? 位置 ダークマター - - 参照レイヤー - - - - - - + + + + + + + + + + + 位置を記録 精度: %1$s m ロケーションプロバイダー: %s diff --git a/strings/src/main/res/values-ka/strings.xml b/strings/src/main/res/values-ka/strings.xml index 27b84218098..1f44b4444a7 100644 --- a/strings/src/main/res/values-ka/strings.xml +++ b/strings/src/main/res/values-ka/strings.xml @@ -210,17 +210,21 @@ tile caches, and are not shown in the UI.--> წყარო ქუჩები - - - - - - - + + + + + + + + + + + უკაცრავად, მდებარეობის მიმღები არ არის გააქტიურებული! @@ -301,6 +305,7 @@ # UNPROTECTED SETTINGS ##############################################--> + სერვერი @@ -382,6 +387,7 @@ მაჩვენე პაროლი გამორთეთ მთავარი მენიუდან დასამალად გადატვირთვა + გადატვირთვა ფორმის შენახვა diff --git a/strings/src/main/res/values-km/strings.xml b/strings/src/main/res/values-km/strings.xml index 3181254c07c..661ead18a4f 100644 --- a/strings/src/main/res/values-km/strings.xml +++ b/strings/src/main/res/values-km/strings.xml @@ -330,17 +330,21 @@ ភ្លឺ ងងឹត មិនអាចចូលដំណើរការផែនទី Google ។ តើសេវា Google Play បានដំឡើងហើយឬនៅ? - - - - - - - + + + + + + + + + + + សុំទោស, សេវាចាប់យកទីតាំងត្រូវបានបិទ រយៈទទឹង: %1$s\nរយៈបណ្ដោយ: %2$s\nរយៈកំពស់: %3$sm\nភាពត្រឹមត្រូវ: %4$sm diff --git a/strings/src/main/res/values-ln/strings.xml b/strings/src/main/res/values-ln/strings.xml index a0a51214083..cff2b9e4668 100644 --- a/strings/src/main/res/values-ln/strings.xml +++ b/strings/src/main/res/values-ln/strings.xml @@ -94,17 +94,21 @@ - - - - - - - + + + + + + + + + + + diff --git a/strings/src/main/res/values-lo-rLA/strings.xml b/strings/src/main/res/values-lo-rLA/strings.xml index 510ae759983..a902f8bd0db 100644 --- a/strings/src/main/res/values-lo-rLA/strings.xml +++ b/strings/src/main/res/values-lo-rLA/strings.xml @@ -177,17 +177,21 @@ - - - - - - - + + + + + + + + + + + ຄຳສັ່ງບັນທຶກຈຸດທີ່ຕັ້ງ GPS ຖືກປິດໃຫ້ໃຊ້ງານ @@ -339,6 +343,7 @@ ປ້ອນລະຫັດຜ່ານຜູ້ຄວບຄຸມລະບົບ ຍົກເລີກການ​ເລືອກ​ ເພື່ອເຊື່ອງຈາກເມນູຫລັກ ຕັ້ງຄ່າຄືນໃໝ່ + ຕັ້ງຄ່າຄືນໃໝ່ - - - - - - - + + + + + + + + + + + Deja, padėties valdikliai yra išjungti! @@ -230,6 +234,7 @@ # UNPROTECTED SETTINGS ##############################################--> + Serveris @@ -299,6 +304,7 @@ Įveskite administratoriaus slaptažodį Uncheck to hide from Main Menu Atstatyti + Atstatyti - - - - - - - + + + + + + + + + + + diff --git a/strings/src/main/res/values-ml/strings.xml b/strings/src/main/res/values-ml/strings.xml index bc55e9b9781..aa6db100435 100644 --- a/strings/src/main/res/values-ml/strings.xml +++ b/strings/src/main/res/values-ml/strings.xml @@ -156,17 +156,21 @@ - - - - - - - + + + + + + + + + + + diff --git a/strings/src/main/res/values-mr/strings.xml b/strings/src/main/res/values-mr/strings.xml index a1f11b3f6c6..e23a5c5a48d 100644 --- a/strings/src/main/res/values-mr/strings.xml +++ b/strings/src/main/res/values-mr/strings.xml @@ -220,17 +220,21 @@ संकरित उपग्रह गूगल नकाशे मध्ये प्रवेश करण्यात अक्षम. गूगल प्ले सेवा स्थापित केली आहे का? - - - - - - - + + + + + + + + + + + क्षमस्व, स्थान प्रदाता अक्षम केले आहेत! अक्षांश: %1$s\nरेखांश: %2$s\nअल्टिट्यूड: %3$sm\nअॅक्चुसीसी: %4$sm diff --git a/strings/src/main/res/values-ms/strings.xml b/strings/src/main/res/values-ms/strings.xml index 5b88e229c66..b0454b61024 100644 --- a/strings/src/main/res/values-ms/strings.xml +++ b/strings/src/main/res/values-ms/strings.xml @@ -91,17 +91,21 @@ - - - - - - - + + + + + + + + + + + diff --git a/strings/src/main/res/values-my/strings.xml b/strings/src/main/res/values-my/strings.xml index b24e0d67717..808c21d2a9e 100644 --- a/strings/src/main/res/values-my/strings.xml +++ b/strings/src/main/res/values-my/strings.xml @@ -167,17 +167,21 @@ - - - - - - - + + + + + + + + + + + တည္ေနရာဖမ္းယူမွု ခ်ိဳ႔ယြင္းေနပါသည္ @@ -321,6 +325,7 @@ စကားဝှက်ပြပါ Uncheck to hide from Main Menu မူလအတုိင္းျပန္ထားရန္ + မူလအတုိင္းျပန္ထားရန္ - - - - - - + + + + + + + + + + + माफ गर्नुहोस्, स्थान प्रदायकहरु निष्क्रिय गरिएका छन् ! @@ -412,6 +416,7 @@ पहिलाकै ठाउँमा ल्याउनुहोस् सबै छान्नुहोस् सबै मेटाउनु होस् + पहिलाकै ठाउँमा ल्याउनुहोस् सबै सेटिङहरू::%s बचत गरिएको फारमहरू:: %s खाली फारमहरू :: %s diff --git a/strings/src/main/res/values-nl/strings.xml b/strings/src/main/res/values-nl/strings.xml index feeb55579dd..9d7a804ba02 100644 --- a/strings/src/main/res/values-nl/strings.xml +++ b/strings/src/main/res/values-nl/strings.xml @@ -275,17 +275,21 @@ Donker Buiten Topografisch - - - - - - - + + + + + + + + + + + Sorry, GPS is uitgeschakeld! Ga naar instellingen diff --git a/strings/src/main/res/values-no/strings.xml b/strings/src/main/res/values-no/strings.xml index 9a0878e9e1d..8d169a53766 100644 --- a/strings/src/main/res/values-no/strings.xml +++ b/strings/src/main/res/values-no/strings.xml @@ -180,17 +180,21 @@ These strings are only being used to preserve compatibility with pre-existing tile caches, and are not shown in the UI.--> Ikke mulig å åpne Google Maps. Er Google Play Services installert? - - - - - - - + + + + + + + + + + + Beklager, stedstjenesten er deaktivert! @@ -267,6 +271,7 @@ # UNPROTECTED SETTINGS ##############################################--> + Server @@ -344,6 +349,7 @@ Enter Admin Password Uncheck to hide from Main Menu Reset + Reset - Warstwa referencyjna - - - - - - + + + + + + + + + + + Zapisz punkt Przykro mi, dostawcy usługi Lokalizacji są niedostępni. Szerokość: %1$s\nDługość: %2$s\nWzniesienie: %3$sm\nDokładność: %4$sm diff --git a/strings/src/main/res/values-ps/strings.xml b/strings/src/main/res/values-ps/strings.xml index 31f6cea8f16..04489cab36c 100644 --- a/strings/src/main/res/values-ps/strings.xml +++ b/strings/src/main/res/values-ps/strings.xml @@ -135,17 +135,21 @@ - - - - - - - + + + + + + + + + + + اوبخښه ، د موقعیت وړاندي کوونکي غیر فعاله دی! diff --git a/strings/src/main/res/values-pt/strings.xml b/strings/src/main/res/values-pt/strings.xml index a683232a479..f9480127071 100644 --- a/strings/src/main/res/values-pt/strings.xml +++ b/strings/src/main/res/values-pt/strings.xml @@ -391,28 +391,27 @@ Não foi capaz de acessar o Google Maps. O serviço do Google Play está instalado? Positron Matéria escura - - Camada de referência - + + Camada - - Selecionar camada de referência - - + + + Saber mais sobre adicionar MBTiles. - - Adicionar camada - - Apagar camada + + + Apagar camada + + + - Camadas + Camadas - Selecionar acesso à camada - Você deseja que a camada esteja disponível em todos os projetos ou somente no projeto atual? - Todos os projetos + Todos os projetos - Apenas o projeto atual + Apenas o projeto atual + Registre um ponto Precisão: %1$s m Provedor de localização: %s diff --git a/strings/src/main/res/values-ro/strings.xml b/strings/src/main/res/values-ro/strings.xml index 456e6028ad2..e52754a351a 100644 --- a/strings/src/main/res/values-ro/strings.xml +++ b/strings/src/main/res/values-ro/strings.xml @@ -189,17 +189,21 @@ Hibrid Satelit Nu am putut accesa Google Maps. Aveți instalat Google Play Services? - - - - - - - + + + + + + + + + + + Ne pare rău, localizarea este dezactivată! @@ -364,6 +368,7 @@ Schimbați parola administratorului Deselectați pentru a ascunde din Meniul principal Resetați + Resetați Da Nu diff --git a/strings/src/main/res/values-ru/strings.xml b/strings/src/main/res/values-ru/strings.xml index fb7e91ebe50..e2ccab8b927 100644 --- a/strings/src/main/res/values-ru/strings.xml +++ b/strings/src/main/res/values-ru/strings.xml @@ -369,18 +369,21 @@ Снаружи Топографический Не удалось получить доступ к Google картам. Установлен ли Google Play Services? - - Базовый слой - - - - - - + + + + + + + + + + + Зафиксировать точку Точность: %1$s м Провайдер геолокации: %s diff --git a/strings/src/main/res/values-rw/strings.xml b/strings/src/main/res/values-rw/strings.xml index b08f0c84591..b7dd0d60af0 100644 --- a/strings/src/main/res/values-rw/strings.xml +++ b/strings/src/main/res/values-rw/strings.xml @@ -353,18 +353,21 @@ N\'ukomea guhura n\'iki kibazo, wakigeza kuwa gusabye gukusanya amakuru.Ntibishoka ko ukoresha ikarita iranga ahantu.Reba ko waba ufite google play muri telefone yawe Utunyangingo. Ibibazo bidasobanutse cg byijimye. - - Indangakitegererezo. - - - - - - + + + + + + + + + + + Irabika amakuru Birizewe: %1$s m Igitanga aho uri ubu: %s diff --git a/strings/src/main/res/values-si/strings.xml b/strings/src/main/res/values-si/strings.xml index 132f135de71..c8c1410c83b 100644 --- a/strings/src/main/res/values-si/strings.xml +++ b/strings/src/main/res/values-si/strings.xml @@ -87,17 +87,21 @@ - - - - - - - + + + + + + + + + + + diff --git a/strings/src/main/res/values-sl/strings.xml b/strings/src/main/res/values-sl/strings.xml index 5738f864e94..595fcad26aa 100644 --- a/strings/src/main/res/values-sl/strings.xml +++ b/strings/src/main/res/values-sl/strings.xml @@ -391,18 +391,21 @@ Ne morem dostopati do Google Maps. Je nameščen Google Play Services? Positron Dark Matter - - Referenčni sloj - - - - - - + + + + + + + + + + + Posnemi točko Natančnost je %1$s m Ponudnik lokacije: %s diff --git a/strings/src/main/res/values-so/strings.xml b/strings/src/main/res/values-so/strings.xml index f4a49d2f405..c2670f63ef7 100644 --- a/strings/src/main/res/values-so/strings.xml +++ b/strings/src/main/res/values-so/strings.xml @@ -99,17 +99,21 @@ - - - - - - - + + + + + + + + + + + diff --git a/strings/src/main/res/values-sq/strings.xml b/strings/src/main/res/values-sq/strings.xml index c61c6a2a27a..60c142a28da 100644 --- a/strings/src/main/res/values-sq/strings.xml +++ b/strings/src/main/res/values-sq/strings.xml @@ -234,17 +234,21 @@ Hibride Satelit E pamundur të aksesohen Google Maps. A është i instaluar Google Play Services? - - - - - - - + + + + + + + + + + + Ofruesit e vendndodhjes janë të ç\'aktivizuar! Gjerësi: %1$s\nGjatësi: %2$s\nLartësi: %3$sm\nSaktësi: %4$sm diff --git a/strings/src/main/res/values-sr/strings.xml b/strings/src/main/res/values-sr/strings.xml index 77e47f1553b..54f62eb0d85 100644 --- a/strings/src/main/res/values-sr/strings.xml +++ b/strings/src/main/res/values-sr/strings.xml @@ -300,17 +300,21 @@ Hibrid Satelit Ne mogu da pristupim Google Mapama. Je li Google Play Service instaliran? - - - - - - - + + + + + + + + + + + Zabilježi lokaciju Pronalaženje lokacije je isključeno! Geografska širina:%1$s\nGeografska dužina: %2$s\nVisina: %3$sm\nPreciznost: %4$sm diff --git a/strings/src/main/res/values-sv-rSE/strings.xml b/strings/src/main/res/values-sv-rSE/strings.xml index 100a78d5702..47828445481 100644 --- a/strings/src/main/res/values-sv-rSE/strings.xml +++ b/strings/src/main/res/values-sv-rSE/strings.xml @@ -309,18 +309,21 @@ Åtkomst till Google Maps ej möjlig. Är Google Play Services installerad? Positron Mörk materia - - Referenslager - - - - - - + + + + + + + + + + + Tyvärr, platstjänster är inaktiverade! Latitud: %1$s\nLongitud: %2$s\nAltitud: %3$sm\nNoggrannhet: %4$sm diff --git a/strings/src/main/res/values-sw-rKE/strings.xml b/strings/src/main/res/values-sw-rKE/strings.xml index 4a8bd26df98..402a8d56b7a 100644 --- a/strings/src/main/res/values-sw-rKE/strings.xml +++ b/strings/src/main/res/values-sw-rKE/strings.xml @@ -178,17 +178,21 @@ These strings are only being used to preserve compatibility with pre-existing tile caches, and are not shown in the UI.--> Barabara - - - - - - - + + + + + + + + + + + Samahani, viwekaji mahali/eneo vimezimwa! @@ -338,6 +342,7 @@ Samahani, password si sawa Weka password ya msimamizi fanya upya + fanya upya - Safu ya Kumbukumbu - - - - - - + + + + + + + + + + + Rekodi pointi Samahani, kionesha mahali hakijawezeshwa! Latitude: %1$s \nLongitude: %2$s \nUrefumwambao:%3$s m\nUsahihi: %4$s m diff --git a/strings/src/main/res/values-te/strings.xml b/strings/src/main/res/values-te/strings.xml index b54a5050433..1dda729d468 100644 --- a/strings/src/main/res/values-te/strings.xml +++ b/strings/src/main/res/values-te/strings.xml @@ -330,18 +330,21 @@ Google మ్యాప్స్‌ను యాక్సెస్ చేయడం సాధ్యం కాట్లేదు. Google Play సేవలు ఇన్‌స్టాల్ చేయబడిందా? Positron Dark Matter - - రిఫరెన్స్ లేయర్ - - - - - - + + + + + + + + + + + ఒక పాయింట్ రికార్డ్ చేయండి %1$s ఖచ్చితత్వం %s ప్రదేశ అనుమతులు diff --git a/strings/src/main/res/values-th-rTH/strings.xml b/strings/src/main/res/values-th-rTH/strings.xml index eff10a28022..7a9d3d7c813 100644 --- a/strings/src/main/res/values-th-rTH/strings.xml +++ b/strings/src/main/res/values-th-rTH/strings.xml @@ -198,17 +198,21 @@ Satellite เปิด Google Maps ไม่ได้ ตรวจสอบว่ามี Google Play Services ติดตั้งหรือไม่ - - - - - - - + + + + + + + + + + + ขออภัย GPS ถูกปิดการใช้งานไว้ diff --git a/strings/src/main/res/values-ti/strings.xml b/strings/src/main/res/values-ti/strings.xml index 859b8c3c22a..ca7e3179de1 100644 --- a/strings/src/main/res/values-ti/strings.xml +++ b/strings/src/main/res/values-ti/strings.xml @@ -101,17 +101,21 @@ - - - - - - - + + + + + + + + + + + diff --git a/strings/src/main/res/values-tl/strings.xml b/strings/src/main/res/values-tl/strings.xml index 36c582395d9..b7157152cf6 100644 --- a/strings/src/main/res/values-tl/strings.xml +++ b/strings/src/main/res/values-tl/strings.xml @@ -146,17 +146,21 @@ - - - - - - - + + + + + + + + + + + Paumanhin, ang mga provider ng Kinalalagyan ay hindi pinagana! @@ -231,6 +235,7 @@ # UNPROTECTED SETTINGS ##############################################--> + Server @@ -300,6 +305,7 @@ Enter Admin Password Uncheck to hide from Main Menu I-reset + I-reset - - - - - - - + + + + + + + + + + + diff --git a/strings/src/main/res/values-uk/strings.xml b/strings/src/main/res/values-uk/strings.xml index 7208fc1729b..a08887366b9 100644 --- a/strings/src/main/res/values-uk/strings.xml +++ b/strings/src/main/res/values-uk/strings.xml @@ -297,17 +297,21 @@ Змішана Супутник Нема доступу до Google Карт. Чи встановлено Google Play Services? - - - - - - - + + + + + + + + + + + Записати точку Вибачте! Джерела місцезнаходження не доступні! Широта: %1$s\nДовгота: %2$s\nВисота: %3$sm\nТочність: %4$sm diff --git a/strings/src/main/res/values-ur/strings.xml b/strings/src/main/res/values-ur/strings.xml index 8778a8563c8..75443a736a0 100644 --- a/strings/src/main/res/values-ur/strings.xml +++ b/strings/src/main/res/values-ur/strings.xml @@ -389,18 +389,21 @@ گوگل میپس تک رسائی حاصل کرنے میں ناکام۔ کیا گوگل پلے سروسز انسٹالڈہے؟ Positron Dark Matter - - ریفرنس Layer - - - - - - + + + + + + + + + + + پوائنٹ کو ریکارڈ کریں درستگی: %1$s میٹر لوکیشن مہیا کرنے والا: %s diff --git a/strings/src/main/res/values-vi/strings.xml b/strings/src/main/res/values-vi/strings.xml index 886f8509546..c0dab011314 100644 --- a/strings/src/main/res/values-vi/strings.xml +++ b/strings/src/main/res/values-vi/strings.xml @@ -149,17 +149,21 @@ - - - - - - - + + + + + + + + + + + Xin lỗi, Khả năng xác định vị trí không được kích hoạt! @@ -235,6 +239,7 @@ # UNPROTECTED SETTINGS ##############################################--> + Máy chủ @@ -304,6 +309,7 @@ Enter Admin Password Uncheck to hide from Main Menu Reset + Reset + 新建表格 + + 草稿 + + 準備傳送 + + 已傳送 + + 下載表格 + + 刪除表格 + 已保存的表格 + 版本: %s + ID: %s + + \'添加於\' EEE, MMM dd, yyyy \' \' HH:mm + + \'更新於\' EEE, MMM dd, yyyy \' \' HH:mm + + \'保存於\' EEE, MMM dd, yyyy \' \' HH:mm + + \'完成於\' EEE, MMM dd, yyyy \' \' HH:mm + + \'傳送於\' EEE, MMM dd, yyyy \' \' HH:mm + + \'傳送失敗於\' EEE, MMM dd, yyyy \' \' HH:mm + + \'於\' EEE, MMM dd, yyyy \' \' HH:mm刪除 + 提交已刪除 + + \'修改日期\' EEE, MMM dd, yyyy \' \' HH:mm + 加密的表單 + 刪除的表單 + 排序列表 + 排序 + 依名稱:A->Z + 依名稱:Z->A + 依日期:由新至舊 + 依日期:由舊至新 + + 依保存時間:由新至舊 + 篩選列表 + + 搜索 + 您嘗試啟動的活動需要未授予的權限。 + 存儲權限 + 沒有這些權限,程式無法存取您的表單或填寫的資料。請授與權限後,再重新開啟此程式。 + 位置權限 + + 沒有 精確位置 權限,Collect 無法記錄您的位置。請授與權限後再重試。 + 相機權限 + 沒有此權限,Collect 無法存取相機。請授與權限後再重試。 + 錄製音頻權限 + 沒有此權限,Collect 無法存取麥克風。請在準備好授予時重試。 + + + 打開表單地圖 + 添加了 %1$d 個表單. + 文件掃瞄結束,所有表格已經被載入。 + %1$s解析出錯: %2$s 缺少或無效。 + 正在載入表格 + 正在讀取表單定義… + 正在讀取調查資料… + 正在讀取CSV文件… + 無法讀取表格。 + 載入表單時出錯。請重試。 + 無法編輯此草稿,因為相應的空白表格不存在或已被刪除。\n\n表格ID: %1$s + 一旦此表格被標記為「最終版」,它便不能再次編輯,因為它可能已被加密。 + + 已從自動儲存點恢復未儲存的修改! + + 更改語言 + + 前往提示 + + 儲存為草稿 + + 開始 + + 結束 + + 返回 + + 群組 + + 可重複群組 + 退出 + + 於 \"%s\"增加項目? + 增加 + 不增加 + 增加其他項目 + + 修改提示 + 刪除針對 \"%s\" 的回答? + 刪除回答 + 刪除這個回答? + 取消 + 刪除回答 + 刪除群組 + 刪除群組 + 刪除這個群組? + 刪除群組 \"%s\" 及其子群組? + 取消 + 回答無效! + 此問題為必答題! + 成功!表格中未發現錯誤。 + 警告:內在值 %s 有空白符號 + 警告:多個內在值 %s 有空白符號 + 此表單對於此設備太複雜。嘗試簡化表達或在論壇上尋求幫助。 + 內部錯誤:提示步驟失敗。 + 您已在 %s 的結尾。 + 最終的表格不可編輯。 + 請注意,傳送後表格不可編輯。 + 如果您需要對表格進行編輯,請點選「儲存為草稿」,直到您準備好傳送為止。 + 瞭解更多 + 儲存為草稿 + 最終確定 + 傳送 + 表格儲存失敗! + 表格已成功儲存! + 錯誤!儲存還原點文件:%s + + 退出 %s + + 放棄表格 + 放棄變更 + 儲存修改 + 繼續編輯 + 儲存表格? + 您可以儲存此表單,並隨時從草稿中存取它。 + \'此表格最後儲存於\' EEE, MMM dd, yyyy\' \'HH:mm\'。另存為草稿以保留變更。\' + 繼續編輯? + 如果您放棄儲存表單,您將失去所有已做的修改。 + \'此表格最後儲存於 \'EEE, MMM dd, yyyy\' \'HH:mm\',如果您放棄變更,將失去此後所做的更改。 + 正在儲存表格 + 正在驗證答案… + 正在收集資料… + 正在儲存至SD卡… + 正儲存到SD卡… + 加密資料 + 此實例中沒有一條記錄! + 並非只有一個空白表單與此 jr_form_id 匹配。 + 未指定表單ID??? + 手機不支持RSA加密。 + RSA公鑰無效。 + %s 表單尚未儲存為完成狀態。 + + 錯誤的URI: %s + + 無法識別的URI: %s + + 無法開始錄製。 + + 麥克風已在使用中。 + + 無法將媒體附加到表單,但可以在 %1$s 獲取 + + 錄製已禁用。在 %s 中啟用 + + 填寫的表單已被刪除! + + 嘗試更新數值失敗,這通常是因為表單設計的 calculation 欄位使用不正確所引起。\n\n如果您一直存在此問題,請向要求您收集資料的人報告。 + + + 執行 + 沒找到與 %s 對應的活動 + 所請求的應用程式無法使用,請手動填入對應值。 + 外部應用程式未提供預期訊息。 + + 列印 + 初始化列印 + 請求的印表機未被安裝. 請安裝印表機. + 打開Url + 啟動 OpenMapKit + 重做OSM標記 + 已編輯的OSM XML文件: + 出了問題,我們沒有獲得有效的OSM資料。 + 警告 + 請安裝OpenMapKit! + 取得條碼 + 替換條碼 + 請將條碼放於矩形內 + 打開文件 + 排序項目 + 數字選擇器 + 此部件的參數無效! + SVG文件不存在! + 拍照 + 選擇圖片 + 所選文件不是有效影像檔案 + 點擊螢幕拍照 + 前置鏡頭在此設備上不可用 + 拍照時出錯 + 無法啟動相機! + 註釋該圖片 + Gif 檔案格式不支援 + 應用程式返回了無效的文件類型 + + 儲存並關閉 + 取得簽名 + 收集簽名 + 修改影像 + 繪製圖像 + 重置 + 選擇顏色 + 拍攝影片 + 選擇影片 + 播放影片 + 選擇文件 + + 刪除 + + 錄音 + + 選擇錄音檔 + + 暫停 + + 恢復 + + 錄音:開啟 + + 錄音:關閉 + 此表單在背景錄音。您必須授予使用麥克風的權限,否則無法開啟此表單。 + + 此表單要求背景錄音,停用它將停止錄製,並放棄現有錄音,請您確定是否繼續? + + 停用錄音 + + 錄音不會立即開始,您必須重新開啟表單進行記錄。 + + 刪除文件? + + 刪除此文件後,您將無法恢復該文件,請您確定是否繼續? + + 停止 + + 正在錄製… + + 錄音 + + 播放聲音 + + 儲存文件 + + 離開此螢幕前必須停止錄音。 + 在存取媒體文件: %s 時發生了錯誤 + + 選擇數值 + 選擇回答 + + 編輯數值 + + 沒有選定的數值 + 選定: + 好的,請繼續。 + 此文件不包含資料! + 列 %s 相符! + sortby列應該只包含數值。衝突值為 \'%s\'. + search() 函數語法錯誤:該函數需要1、4或6個參數。 + search() 函數中的語法錯誤:未識別的函數 \'%s\'. + search()函數中的語法錯誤: \'%s\' 未作為函數計算. + 搜索處理程式返回了類型為 \'%s\'的對象. + 無法從 %1$s 中導入資料,原因:%2$s + 從中 \'%1$s\'預載入資料, 請稍候… %2$s + 讀取資料已取消! + 正在完成預載入的資料… + 讀取資料已完成! + ExternalDataManager尚未初始化。 + 外部資料 %1$s 尚未導入. 也許您忘記在表單中包含 %2$s.csv 文件? + search() 函數中的語法錯誤: %s + \'%s\' 無法賦予值。 + XPathParser 異常: \"%s\" + 記錄方位 + 替代方位 + 載入方位 + 記錄方位 + 方位: %.3f + 方向: %s + 無法收集方位資訊:缺少加速計或磁場傳感器設備,或兩者都缺。 + 選擇日期 + 選擇時間 + 未選擇日期 + 未選擇時間 + Meskerem + Tikimt + Hidar + Tahsas + Tir + Yekatit + Megabit + Miazia + Ginbot + Senie + Hamlie + Nehasie + Pagumien + Thout + Paopi + Hathor + Koiak + Tobi + Meshir + Paremhat + Parmouti + Pashons + Paoni + Epip + Mesori + Pi Kogi Enavot + Muharram + Safar + Rabi\' al-awwal + Rabi\' al-thani + Jumada al-awwal + Jumada al-thani + Rajab + Sha\'ban + Ramadan + Shawwal + Dhu al-Qidah + Dhu al-Hijjah + Farvardin + Ordibehesht + Khordad + Tir + Mordad + Shahrivar + Mehr + Aban + Azar + Dey + Bahman + Esfand + + %1$s (%2$s) + + 文件: %s 不是有效文件。 + + 文件: %s 不能被找到。 + + 輸入身份 + + 身份 + + 此表單要求您確認身份 + + 關閉 + + 變更原因 + + 理由 + + 您需要解釋更改此表單的原因 + + 儲存 + + 選擇地點 + 選擇 + + + USGS地形圖 + USGS混合地圖 + USGS影像地圖 + CartoDB Positron + CartoDB Dark Matter + 底圖 + 來源 + 抱歉, %s 此設備上沒有底圖。請在「設置」中選擇另一個底圖 > 地圖. + %s 地圖樣式 + 街道 + 地形 + 混合 + 衛星 + 淺色 + 深色 + 戶外 + 地形圖 + 無法存取谷歌地圖。是否安裝了Google Play服務? + Positron + Dark Matter + + 離線圖層 + + 圖層 + + 選擇離線圖層 + + 選擇要用於該項目中所有地圖的離線圖層。您可以通過從設備中選擇 MBTiles 文件來將選項添加到列表中,也可以從列表中刪除現有圖層。 + + 瞭解有關添加 MBTiles 的更多訊息。 + + 新增圖層 + + 刪除圖層 + + + %d 圖層無法新增 + + + 您選擇的文件不是 MBTiles。您只能添加 MBTiles 文件。 + + 某些您選擇的文件不是 MBTiles 格式,您只能添加 MBTiles 文件。 + + 圖層 + + 選擇存取圖層 + 您希望圖層在所有項目中使用,還是僅在當前項目中使用? + + 所有項目 + + 目前項目 + + 您確定要刪除 %1$s 離線圖層嗎? + 記錄點 + 精確度: %1$s m + 位置提供程式: %s + 位置服務被關閉! + 緯度: %1$s\n經度: %2$s\n高程: %3$sm\n精度: %4$sm + 此表單希望跟蹤您的位置,但Google Play服務不可用。 + 此表單希望跟蹤您的位置,但位置提供程式被禁用,請在Android設置中啟用。 + + 轉到設置 + + 追蹤位置:開啟 + + 追蹤位置:關閉 + 此表單希望跟蹤您的位置,但已禁用跟蹤,請在選單 %1$s 上啟用 + 此表單跟蹤您的位置,您可以選單上 %1$s 中禁用跟蹤。 + + 檢查錯誤 + + 已用時間:%1$s + + 點將儲存在 %1$s + + 衛星:%1$s + 正在嘗試獲取位置,請稍候。 + 精度差,請稍候。 + 精度不可接受,請稍候。 + 提高準確性,請稍候。 + 正在獲取位置 + + %1$sm + + W %1$s%2$s%3$s + + E %1$s%2$s%3$s + + S %1$s%2$s%3$s + + N %1$s%2$s%3$s + GeoShape + 輸入方法 + 開始 + 啟用GPS + GeoTrace + 設備上的GPS已禁用,是否要啟用它? + 對像已建立,是否要清除該對像? + 放棄更改並返回 ODK? + 必須至少有3個點才能建立多邊形 + 必須至少有2個點才能建立多段線 + 清除 + 輕敲放置 + 手動位置記錄 + 自動位置記錄 + 記錄間隔: + + + %d 秒 + + + + %d 分鐘 + + 精度要求: + 沒有 + + + %d 米 + + 正在搜索位置,請稍候… + 位置精度: %.2f m + 位置精度: %.2f m (可接受) + 位置精度: %.2f m (不可接受) + 輸入的點數: %d + 輸入的點數: %d (點擊以放置點) + 輸入的點數: %d (手動記錄) + 輸入的點數: %1$d (每隔%2$d 秒記錄) + 輸入的點數: %1$d (每隔 %2$d 分鐘記錄) + 輸入的點數: %1$d (每隔 %2$d 秒記錄, 精度%3$d m) + 輸入的點數: %1$d (每隔 %2$d 分鐘記錄, 精度 %3$d m) + 長按放置標記或點擊添加標記按鈕。 + 點擊添加標記按鈕。 + 取消 + %s: %d (%d 在圖上) + 選擇 + 新項目 + + 編輯草稿 + 查看已儲存的表格 + + 位置跟蹤 + + 跟蹤位置… + + 顯示位置 + + 縮放至適合所有 + + 暫停 + + 移除最後一點 + + 正在傳送表格 + 沒有選定表格! + 沒有可用網路 + 上傳 %2$s 中的 %1$s 個表格 + 上傳選定項 + 伺服器:%s無效的使用者名稱或密碼 + 伺服器要求驗證身份 + 沒有表格被上傳。 + 上傳結果 + + 修改視圖 + 顯示已傳送和未傳送的表格 + 只顯示未傳送的表格 + 後台傳送正在運行,請稍後重試 + 表單上載成功 + 表單上載失敗 + 所有上傳成功! + %1$s / %2$s 上載失敗! + + 更新 + 下載選取項目 + 連接到伺服器 + 獲取 %1$s.\n\n表單 %2$s / %3$s 表單。 + %1$s. 載入媒體文件: %3$s 中的 %2$s + 版本: + 表單更新可用 + 這是對您的表單的更新 + 表單更新失敗 + 表單更新成功 + 表單列表下載失敗 + 表單下載成功 + 表單下載失敗 + 所有下載成功! + %1$s / %2$s 下載失敗! + + ID: %1$s 版本: %2$s + + 如果你一直有這個問題,請向要求你收集資料的人報告。 + + 無法在存取伺服器 %s,您輸入的URL是否正確? + + 無法安全連接到位於 %s 的伺服器. 您輸入的URL是否正確? + + 伺服器 %1$s 返回的狀態 %2$s。 + + 伺服器 %s 提供了無效的響應。 + + 伺服器未為此表單提供哈希值。 + + 無法處理此表單。 + + 下載此表單時,設備上出現磁碟錯誤。 + + 此表單的提交URL無效。 + + + 已儲存的表格 + + 空表格 + 刪除表格 %s ? + 刪除選中 + 不刪除 + 刪除表格 + 刪除所選表單 + %s 表格被成功刪除! + + 項目設置 + + 伺服器 + + URL、使用者名稱、密碼 + + 項目顯示 + + 名稱、圖示、顏色 + + 使用者介面 + + 應用程式語言、主題、字體大小 + + 地圖 + + 底圖、樣式、圖層 + + 表單管理 + + 自動更新、自動傳送、自動刪除 + + 使用者和設備標識 + + 使用者名稱、電話號碼、設備ID + + 實驗項目 + 受保護的設置 + + 解鎖受保護的設置 + + 更改密碼,設置存取控制 + 設置管理員密碼 + + 項目管理 + + 重新配置、重置、刪除 + + 存取控制 + + 有限使用者界面 + 伺服器設置 + + URL + + 伺服器 URL + 不正確的URL! + 使用者名稱 + 使用者名稱 + 密碼 + 密碼 + 密碼字串前後不能有空格 + 使用者名稱字串前後不能有空格 + 主題 + 淺色主題 + 深色主題 + 使用設備主題 + 語言 + 使用設備語言 + 字體大小 + 字體大小 + 極大 + + + + 極小 + 操作方式 + 使用螢幕左右滑動 + 使用前進/後退按鈕 + 使用滑動和按鈕 + + 下一個 + + 上一個 + + 表單更新 + + 空白表單更新模式 + + 手動 + + 僅限以前下載的表單 + + 與伺服器完全相符 + + 自動更新頻率 + + 每十五分鐘 + + 每小時 + + 每6小時 + + 每24小時 + + 自動下載 + 自動下載表單的更新版本 + 隱藏舊版本表單 + 只有最新版本才會出現在「填寫空白表單」中 + 表單提交 + 自動傳送 + 自動傳送 + 關閉 + 僅Wi-Fi + 僅行動網路 + Wi-Fi或行動網路 + 傳送後自動刪除 + 傳送到伺服器後刪除已完成的表單和媒體 + 表單填寫 + 約束處理 + 約束檢查 + 向前滑動時驗證 + 推遲檢驗直到完成 + 高解析度影片 + 啟用高解析度影片錄製 + 相片大小 + 相片長的最高像素值 + 大(3072px) + 中(2048px) + 小(1024px) + 非常小(640px) + 相機的原始大小(默認) + + 顯示指南 + + 是-始終顯示 + + 是-已折疊 + + 使用外部應用程式進行音頻錄製 + 表單導入 + 導入時完成表單 + 影響直接添加到實例文件夾的表單。 + 使用情況資料 + 收集匿名使用資料 + 匿名使用資料幫助團隊確定修復和功能的優先級。 + 表單元資料 + 表單元資料 + 這些值將添加到預先載入了使用者名稱、電子郵件和/或電話字段的表單中,以標識提交資料的人。 + 使用者定義的 + 電話號碼 + 電子郵件地址 + 電子郵件地址無效! + 設備已定義 + 設備ID + + 不可用 + + 解鎖設置 + + 設置已解鎖 + + 輸入新密碼 + 管理員密碼 + 管理員密碼已成功修改 + 管理員密碼已禁用 + 對不起, 密碼不正確! + 輸入管理員密碼 + 更改管理員密碼 + 顯示密碼 + 不選中以從主菜單中隱藏 + 取消選中以隱藏不受保護的設置 + 取消選中以隱藏表單條目 + 取消選中以隱藏 Form End + 重置 + 從設置、表單和資料中進行選擇 + 刪除 + 全選 + 清空 + 所有設置(內部設置、儲存的設置) + 儲存的表單(實例文件夾、實例資料庫) + 空白表單(表單文件夾、表單資料庫、元素集資料庫) + 地圖圖層(圖層文件夾) + 表單載入緩存(.cache文件夾) + 選擇要重置的內容 + 重置 + 正在重置… + 重置結果 + 所有選定的資料將被永久刪除。無法撤消。 + 所有設置 :: %s + 儲存的表單 :: %s + 空白表單 :: %s + 地圖圖層 :: %s + 表單載入緩存 :: %s + 主菜單設置 + 顯示或隱藏按鈕 + 使用者設置 + 顯示或隱藏設置 + 表單條目設置 + 顯示或隱藏操作 + + 向後移動 + 向後移動已禁用 + 配置設備以防止使用者繞過此設置?\n\n更改如下:\n\u2022 禁用「編輯草稿」\n\u2022 禁用「另存為草稿」\n\u2022 禁用「轉到提示」\n\u2022 將約束處理設置為向前滑動時驗證 + + + 已啟用向後移動 + 您可能希望檢查以下設置:\n\n\u2022 編輯草稿\n\u2022 另存為草稿\n\u2022 前往提示\n\u2022 約束處理 + + 儲存為草稿 + 退出表單時,頂部欄中的儲存圖標和「另存為草稿」按鈕 + + 關於 + 分享ODK Collect + 您的同事還在用紙本收集資料嗎?與他們分享 ODK Collect 應用程式。 + 留下 Play Store 評論 + 你的評論(希望是正面的)提高了應用程式在應用商店中的知名度。 + 造訪 ODK 網站 + ODK 用於具有挑戰性的環境中收集社會資料。 + 加入論壇 + 加入 ODK 論壇以獲得支持、請求功能、貢獻代碼/翻譯! + 開源庫/許可證 + 我們站在巨人的肩膀上! + + 使用 QR code 重新配置 + 替換所有設置 + QR code 配置 + 掃瞄 + QR code + 導入 QR code + 成功導入設置 + 已達到最大字符數限制:無法為所有設置生成 QR code + 分享 + 打開閃光燈 + 關閉閃光燈 + 包含敏感資訊:管理員伺服器密碼 + QR code 不包含 管理員伺服器 密碼。點擊以配置 + 包含敏感資訊:管理員 密碼 + 包含敏感資訊:伺服器 密碼 + 伺服器密碼 + 代碼中包含的密碼 + 生成 + QR code 不包含有效設置 + 在所選圖像中找不到 QR code + 當前設置已損壞。從項目管理設置中,重置設置或導入工作設置。 + 無法再建立 Google Drive/Sheets 項目 + + 確定 + + 取消 + 取消 + 取消 + 取消 + 取消 + 正在取消 + 一般通知 + 成功 + 錯誤 + 請稍候。 + 請稍候。這可能需要幾分鐘。 + 無法刪除 \'%s\'. 請手動刪除文件並重新下載表單。 + 無法建立媒體文件夾 \'%s\'. + 不能複製 \'%1$s\' 到 \'%2$s\',原因: %3$s + 您下載了兩個具有相同表單ID和版本的不同表單。也許它們是在不同的時間或不同的伺服器上傳的相同表單。無論如何,您都應該刪除一個。 + 選擇 ODK 快捷方式 + 此表單記錄設備麥克風的音頻。\n\n您可以使用音量指示器來確保麥克風足夠接近您需要錄製的聲音。\n\n若要停止錄製,請退出此表單。\n\n有關詳細資訊,請與要求您收集資料的人員聯繫。 + 正在載入… + + 項目 + 設置 + 添加項目 + 添加 + + 重複項目 + + 您已經有此連結設置的項目。要切換到現有項目還是添加新項目? + + 添加重複項目 + + 切換到現有項目 + 項目名稱 + 項目圖示 + 項目顏色 + + 已切換到 %s + + 使用 %s + + 切換到 %s + 所有空白表單、提交內容和設置將被永久刪除。 + + + 無法刪除項目 + 您有未傳送的提交內容,若要刪除項目,必須先傳送或刪除這些提交內容。 + 後台作業正在運行。請稍後再試。 + + 十六進位顏色 + 無效的十六進位代碼 + + + 收集資料\n任何位置 + + 使用 QR code 配置 + + 手動輸入項目詳細訊息 + 在新增項目後,可以於「設置」對項目進行設定 + + 還沒有項目? + + 嘗試示範項目 + 掃瞄配置 QR code + + 要打開此表單,必須首先開啟 ODK Collect 並切換到包含它的項目。 + + 尚未配置 ODK Collect 。嘗試開啟 ODK Collect 進行設定。\n\n如果您點選了快捷方式,您可能需要在配置 ODK Collect 後重新建立它。 + + 顯示詳細訊息 + 錯誤 + + 打開設置 + + 實體 + + 啟用本地實體 + 後續表格將具有一致的實體列表,並將包括本地創建或更新的實體。 + + 查看實體列表 + + 全部清除 + + 新增實體列表 + + 離線 + + 應用程式上次運行時發生了崩潰! + 無法啟動應用程式! + + 該項目之前已連接到 Google Drive 帳戶。Google Drive 支持已被刪除。您可以配置伺服器或將提交內容傳至電腦。 + + + 瞭解更多 + + + 開發人員工具 + + 崩潰應用程式 + + 強制執行導致應用程式崩潰的未捕獲異常 + 關於權限 + 您將被要求允許 ODK Collect 存取以下功能,如果您想使用它們,請選擇「允許」。 + 提醒事項 + 在下載、更新和傳送表單時需要顯示更新。 + + 您的表單已儲存為草稿。 + 你的表格已被儲存。 + 傳送表格中… + 編輯 + 檢視 + 關閉通知 + + + 最後傳送的表格:%d 秒前 + + + 最後傳送的表格:%d 分鐘前 + + + 最後傳送的表格:%d 小時前 + + + 最後傳送的表格:%d 天前 + + + %d 表格已準備傳送 + + 在後續版本中,最終確定的表格將不再可編輯。將表單儲存為草稿以便稍後編輯。\n\n您可以通過點擊三個點 (⋮) 然後檢查錯誤來檢查草稿表單中的錯誤。 + + 在後續版本中,最終確定的表格將不再可編輯。 + + 完成所有草稿 + + + 您要完成 %d 草稿? + + + 一旦您完成所有草稿,它們將處於「準備傳送」狀態,並且您將無法進行編輯。任何有錯誤的草稿都不會被最終確定。\n\n您將無法撤消此操作。 + + + 成功!%d 份草稿已完成。 + + + + %d 個草稿有錯誤,必須在最終確定之前解決。 + + + %d 份草稿已完成。%d 個草稿有錯誤,必須在最終確定之前解決。 + + %d 份草稿已完成。剩下的草稿需要手動完成。 + + 錯誤 + + 無錯誤 + + 取消選中即可在草稿中隱藏 + + 新功能 + + 草稿列表現在已顯示驗證錯誤。每次將表單儲存為草稿時,其驗證狀態都會更新。\n\n標有「錯誤」的草稿要麼缺少必填問題,要麼包含不允許的值。 + + 沒有可顯示的內容 + + 無空白表格 + + 下載表格以開始使用 + + 無草稿 + + 當您儲存為草稿時,表格將顯示在此處 + + 尚無準備傳送的表單 + + 當你完成草稿時,它們將顯示在這裡 + + 沒有任何已傳送的表單 + + 當您傳送最終表單後,它們將在這裡顯示 + + 無下載表單 + 下載表格以開始使用 + + 沒有要刪除的表單 + + 下載空白表單後,它們將顯示在這裡 + + 當您儲存表單後,它們將顯示在此處 + + + 恢復您的作業? + + \'Collect closed unexpectedly on \'EEE, MMM dd, yyyy \'at\' HH:mm\' and saved your work.\n\nWould you like to recover or discard it?\' +\'Collect 於 \'EEE, MMM dd, yyyy \' \' HH:mm\' 意外關閉並儲存了您的工作。\n\n您想恢復還是放棄它? + + 恢復 + + 放棄 + + 取得定位點 + + 檢視或查看定位點 + + 查看定位點 + + 變更定位點 + + 取得線段 + + 查看或變更線段 + + 檢視線段 + + 獲取多邊形 + + 檢視或變更多邊形 + + 檢視多邊形 + diff --git a/strings/src/main/res/values-zh/strings.xml b/strings/src/main/res/values-zh/strings.xml index 1ada24ddd74..86d9a8f9f93 100644 --- a/strings/src/main/res/values-zh/strings.xml +++ b/strings/src/main/res/values-zh/strings.xml @@ -69,6 +69,7 @@ 正在读取CSV文件… 无法读取表格. 加载表单时出错。请重试。 + 无法编辑此草稿,因为相应的空白表格不存在或已被删除。\n\n表格ID: %1$s 一旦此表格被标记为“最终版”,它便不能再次编辑,因为它可能已被加密。 已从自动保存点恢复未保存修改! @@ -114,7 +115,12 @@ 此表单对于此设备太复杂。尝试简化表达或在论坛上寻求帮助。 内部错误:提示步骤失败。 您已在 %s 的末尾. + 最终的表格不可编辑。 + 发送的表格不可编辑。 + 如果您需要对表格进行编辑,请“另存为草稿”,直到您准备好发送为止。 + 了解更多 保存为草稿 + 最终确定 发送 表格保存失败! 表格已成功保存! @@ -122,7 +128,16 @@ 退出 %s + 放弃表格 + 放弃变更 保存修改 + 继续编辑 + 储存表格? + 您可以保存此表单并随时从草稿中存取它。 + \'此表格最后储存于\' EEE, MMM dd, yyyy\'at\'HH:mm\'。另存为草稿以保留变更。\' + 继续编辑? + 如果您丢弃表单,您将失去所有已做的更改。 + \'此表格最后保存于 \'EEE, MMM dd, yyyy\'at\'HH:mm\'。如果您放弃更改,您将失去此后所做的所有更改。 正在保存表格 正在验证答案… 正在收集数据… @@ -160,6 +175,7 @@ 请求的应用不可用. 请手动填入相应值. 外部应用程序未提供预期信息。 + 列印 初始化打印 请求的打印机未被安装. 请安装打印机. 打开Url @@ -182,7 +198,10 @@ 所选文件不是有效图像 点击屏幕拍照 前置摄像头在此设备上不可用 + 拍照时出错 + 无法启动相机! 注释该图片 + Gif 档案格式不支援 应用程序返回了无效的文件类型 保存并关闭 @@ -207,7 +226,9 @@ 恢复 + 录音:开 + 录音:关闭 此窗体在后台录制音频。您必须授予使用麦克风的权限。否则,您将无法打开此表单。 此表单请求后台音频录制。禁用它将停止录制并丢弃现有音频。是否确实要继续? @@ -370,18 +391,39 @@ 无法访问谷歌地图。是否安装了Google Play服务? Positron Dark Matter - - 应用图层 - - - - - - + + 离线图层 + + 图层 + + 选择离线图层 + + 选择要用于该项目中所有地图的离线图层。您可以通过从设备中选择 MBTiles 文件来将选项添加到列表中,也可以从列表中删除现有图层。 + + 了解有关添加 MBTiles 的更多信息。 + + 新增图层 + + 删除图层 + + + %d 图层无法新增 + + + 您选择的文件不是 MBTiles。您只能添加 MBTiles 文件。 + + 某些您选择的文件不是 MBTiles 格式。您只能添加 MBTiles 文件。 + 图层 + 选择访问图层 + 您希望图层在所有项目中使用,还是仅在当前项目中使用? + 所有项目 + 目前项目 + + 您确定要删除 %1$s 离线图层吗? 记录点 精确度: %1$s m 位置提供程序: %s @@ -392,7 +434,9 @@ 转到设置 + 追踪位置:开 + 追踪位置:关 此表单希望跟踪您的位置,但已禁用跟踪. 请在菜单 %1$s 上启用他 此表单跟踪您的位置. 您可以在上面的菜单 %1$s 中禁用跟踪。 @@ -721,6 +765,7 @@ 不选中以从主菜单中隐藏 取消选中以隐藏不受保护的设置 取消选中以隐藏表单条目 + 取消选中以隐藏 Form End 重置 从设置、表单和数据中进行选择 删除 @@ -750,11 +795,14 @@ 向后移动 向后移动已禁用 + 配置设备以防止用户绕过此设置?\n\n更改如下:\n\u2022 禁用“编辑草稿”\n\u2022 禁用“另存为草稿”\n\u2022 禁用“转到提示”\n\u2022 将约束处理设置为向前滑动时验证 已启用向后移动 + 您可能希望检查以下设置:\n\n\u2022 编辑草稿\n\u2022 另存为草稿\n\u2022 前往提示\n\u2022 约束处理 保存为草稿 + 退出表单时,顶部栏中的保存图标和“另存为草稿”按钮 @@ -793,6 +841,7 @@ 二维码不包含有效设置 在所选图像中找不到二维码 当前设置已损坏。从项目管理设置中,重置设置或导入工作设置。 + 无法再创建 Google Drive/Sheets 项目 @@ -881,10 +930,16 @@ 实体 + 启用本地实体 + 后续表格将具有一致的实体列表,并将包括本地创建或更新的实体。 + 查看实体列表 + 全部清除 + 新增实体列表 + 离线 @@ -893,6 +948,10 @@ + 该项目之前已连接到 Google Drive 帐户。Google Drive 支持已被删除。您可以配置服务器或将提交内容传至电脑。 + + + 了解更多 @@ -902,59 +961,127 @@ 崩溃应用程序 强制执行导致应用程序崩溃的未捕获异常 + 关于权限 + 您将被要求允许 ODK Collect 访问以下功能,如果您想使用它们,请选择“允许”。 提醒事项 + 在下载、更新和发送表单时需要显示更新。 + 您的表单已保存为草稿。 你的表格已被保存。 发送表格中… 编辑 + 检视 + 关闭通知 + + 最后发送的表格:%d 秒前 + + + 最后发送的表格:%d 分钟前 + + + 最后发送的表格:%d 小时前 + + + 最后发送的表格:%d 天前 + + + %d 表格已准备传送 + + 在后续版本中,最终确定的表格将不再可编辑。将表单保存为草稿以便稍后编辑。\n\n您可以通过点击三个点 (⋮) 然后检查错误来检查草稿表单中的错误。 + 在后续版本中,最终确定的表格将不再可编辑。 + 完成所有草稿 + + 您要完成 %d 草稿? + + 一旦您完成所有草稿,它们将处于“准备发送”状态,并且您将无法进行编辑。任何有错误的草稿都不会被最终确定。\n\n您将无法撤消此操作。 + + 成功!%d 份草稿已完成。 + + + %d 个草稿有错误,必须在最终确定之前解决。 + + %d 份草稿已完成。%d 个草稿有错误,必须在最终确定之前解决。 + %d 份草稿已完成。剩下的草稿需要手动完成。 错误 + 无错误 + 取消选中即可在草稿中隐藏 + 新功能 + 草稿列表现在已显示验证错误。每次将表单保存为草稿时,其验证状态都会更新。\n\n标有“错误”的草稿要么缺少必填问题,要么包含不允许的值。 + 没有可显示的内容 + 無空白表格 + 下载表格以开始使用 + 無草稿 + 当您保存为草稿时,表格将显示在此处 + 尚无准备发送的表單 + 当你完成草稿时,它们将显示在这里 + 沒有任何已發送的表單 + 当您发送最终表單後,它们将在这里显示 + 无下载表單 + 下载表格以开始使用 + 没有要删除的表單 + 下载空白表单后,它们将显示在这里 + 当您保存表单后,它们将显示在此处 + 恢复您的作业? + \'Collect closed unexpectedly on \'EEE, MMM dd, yyyy \'at\' HH:mm\' and saved your work.\n\nWould you like to recover or discard it?\' +\'Collect 于 \'EEE, MMM dd, yyyy \' \' HH:mm\' 意外关闭并保存了您的工作。\n\n您想恢复还是放弃它? + 恢复 - 丢弃 + 放弃 + 取得定位点 + 检视或查看定位点 + 查看定位点 + 变更定位点 + 取得线段 + 查看或变更线段 + 检视线段 + 获取多边形 + 检视或变更多边形 + 检视多边形 diff --git a/strings/src/main/res/values-zu/strings.xml b/strings/src/main/res/values-zu/strings.xml index 7e5c74c0bd5..08baa541b3f 100644 --- a/strings/src/main/res/values-zu/strings.xml +++ b/strings/src/main/res/values-zu/strings.xml @@ -160,17 +160,21 @@ - - - - - - - + + + + + + + + + + + Uxolo, indawo enikeziwe ayisebenzi @@ -315,6 +319,7 @@ Faka inombolomfihlo yokusingathwa komsebenzi Susa uphawu ukufihla ohlwini oluwumongo Hlela kabusha + Hlela kabusha Ihre Arbeit Wiederherstellen? - Collect wurde am \'EEE, MMM dd, yyyy \'um\' HH:mm\' unerwartet beendet und hat ihre Arbeit gespeichert.\n\nWollen Sie die gespeicherte Arbeit wiederherstellen oder verwerfen? + \'Collect wurde am \'EEE, MMM dd, yyyy \'um\' HH:mm\' unerwartet beendet und hat ihre Arbeit gespeichert.\n\nWollen Sie die gespeicherte Arbeit wiederherstellen oder verwerfen?\' Wiederherstellen diff --git a/strings/src/main/res/values-fr/strings.xml b/strings/src/main/res/values-fr/strings.xml index bd6f909cdb2..4975313d24e 100644 --- a/strings/src/main/res/values-fr/strings.xml +++ b/strings/src/main/res/values-fr/strings.xml @@ -1080,7 +1080,7 @@ Récupérer votre travail ? - \'Collect s\'est arrété de manière inattendue le \'EEE dd MMM yyyy à HH:mm\' et a sauvegardé votre travail.\n\nVoulez vous le récupérer ou l\'ignorer ?\' + \'Collect s\'\'est arrété de manière inattendue le \'EEE dd MMM yyyy\' à \'HH:mm\' et a sauvegardé votre travail.\n\nVoulez vous le récupérer ou l\'\'ignorer ?\' Récupérer ? diff --git a/strings/src/main/res/values-zh-rTW/strings.xml b/strings/src/main/res/values-zh-rTW/strings.xml index c3b734a1ce1..771eb91d4e0 100644 --- a/strings/src/main/res/values-zh-rTW/strings.xml +++ b/strings/src/main/res/values-zh-rTW/strings.xml @@ -137,7 +137,7 @@ \'此表格最後儲存於\' EEE, MMM dd, yyyy\' \'HH:mm\'。另存為草稿以保留變更。\' 繼續編輯? 如果您放棄儲存表單,您將失去所有已做的修改。 - \'此表格最後儲存於 \'EEE, MMM dd, yyyy\' \'HH:mm\',如果您放棄變更,將失去此後所做的更改。 + \'此表格最後儲存於 \'EEE, MMM dd, yyyy HH:mm\',如果您放棄變更,將失去此後所做的更改。\' 正在儲存表格 正在驗證答案… 正在收集資料… @@ -1058,8 +1058,7 @@ 恢復您的作業? - \'Collect closed unexpectedly on \'EEE, MMM dd, yyyy \'at\' HH:mm\' and saved your work.\n\nWould you like to recover or discard it?\' -\'Collect 於 \'EEE, MMM dd, yyyy \' \' HH:mm\' 意外關閉並儲存了您的工作。\n\n您想恢復還是放棄它? + \'Collect 於 \'EEE, MMM dd, yyyy HH:mm\' 意外關閉並儲存了您的工作。\n\n您想恢復還是放棄它?\' 恢復 diff --git a/strings/src/main/res/values-zh/strings.xml b/strings/src/main/res/values-zh/strings.xml index 86d9a8f9f93..ccba4117299 100644 --- a/strings/src/main/res/values-zh/strings.xml +++ b/strings/src/main/res/values-zh/strings.xml @@ -134,10 +134,10 @@ 继续编辑 储存表格? 您可以保存此表单并随时从草稿中存取它。 - \'此表格最后储存于\' EEE, MMM dd, yyyy\'at\'HH:mm\'。另存为草稿以保留变更。\' + \'此表格最后储存于\' EEE, MMM dd, yyyy \'at\' HH:mm\'。另存为草稿以保留变更。\' 继续编辑? 如果您丢弃表单,您将失去所有已做的更改。 - \'此表格最后保存于 \'EEE, MMM dd, yyyy\'at\'HH:mm\'。如果您放弃更改,您将失去此后所做的所有更改。 + \'此表格最後儲存於\' EEE, MMM dd, yyyy HH:mm\',如果您放棄變更,將失去此後所做的更改。\' 正在保存表格 正在验证答案… 正在收集数据… @@ -1058,8 +1058,7 @@ 恢复您的作业? - \'Collect closed unexpectedly on \'EEE, MMM dd, yyyy \'at\' HH:mm\' and saved your work.\n\nWould you like to recover or discard it?\' -\'Collect 于 \'EEE, MMM dd, yyyy \' \' HH:mm\' 意外关闭并保存了您的工作。\n\n您想恢复还是放弃它? + \'Collect 于 \'EEE, MMM dd, yyyy HH:mm\' 意外关闭并保存了您的工作。\n\n您想恢复还是放弃它?\' 恢复 From 1dbe8ad8cb94effb7c5f004e31668c77f616010c Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 1 Jul 2024 14:48:01 +0200 Subject: [PATCH 710/750] Dismiss the confirmation dialog on activity recreation --- .../org/odk/collect/maps/layers/OfflineMapLayersPicker.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 1575b8e07b5..2e0a3f5a035 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -116,6 +116,13 @@ class OfflineMapLayersPicker( dismiss() } + if (sharedViewModel.layersToImport.value?.value == null) { + DialogFragmentUtils.dismissDialog( + OfflineMapLayersImporter::class.java, + childFragmentManager + ) + } + return binding.root } From e06589fdceff8c983ff465e2595db397051f07c9 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 1 Jul 2024 14:48:12 +0200 Subject: [PATCH 711/750] Added a test --- .../maps/layers/OfflineMapLayersPickerTest.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index 9b540cf73db..1a4132092f3 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -823,6 +823,30 @@ class OfflineMapLayersPickerTest { ) } + @Test + fun `the confirmation dialog is dismissed o activity recreation`() { + val scenario = launchFragment() + + uris.add(Uri.parse("blah")) + Interactions.clickOn(withText(string.add_layer)) + + scenario.onFragment { + assertThat( + it.childFragmentManager.findFragmentByTag(OfflineMapLayersImporter::class.java.name), + instanceOf(OfflineMapLayersImporter::class.java) + ) + } + + scenario.recreate() + + scenario.onFragment { + assertThat( + it.childFragmentManager.findFragmentByTag(OfflineMapLayersImporter::class.java.name), + equalTo(null) + ) + } + } + private fun launchFragment(): FragmentScenario { return fragmentScenarioLauncherRule.launchInContainer(OfflineMapLayersPicker::class.java) } From e9905035e7c51b62d1202b3787fdb2b6881c922c Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 3 Jul 2024 09:30:10 +0200 Subject: [PATCH 712/750] Added a failing test --- .../android/utilities/ExternalAppIntentProviderTest.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/collect_app/src/test/java/org/odk/collect/android/utilities/ExternalAppIntentProviderTest.kt b/collect_app/src/test/java/org/odk/collect/android/utilities/ExternalAppIntentProviderTest.kt index 02b26629420..765725f7c40 100644 --- a/collect_app/src/test/java/org/odk/collect/android/utilities/ExternalAppIntentProviderTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/utilities/ExternalAppIntentProviderTest.kt @@ -74,4 +74,12 @@ class ExternalAppIntentProviderTest { assertThat(resultIntent.extras!!.getString("param1"), `is`("value1")) assertThat(resultIntent.extras!!.getString("param2"), `is`("value2")) } + + @Test + fun parametersCanContainParentheses() { + whenever(formEntryPrompt.appearanceHint) + .thenReturn("ex:com.example.collectanswersprovider(param='blah()')") + val resultIntent = externalAppIntentProvider.getIntentToRunExternalApp(null, formEntryPrompt) + assertThat(resultIntent.extras!!.getString("param"), `is`("blah()")) + } } From bb6cec2d31e13aadf0c51819fe5c660d6f5a64fb Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 3 Jul 2024 09:32:26 +0200 Subject: [PATCH 713/750] Fixed ExternalAppIntentProvider --- .../collect/android/utilities/ExternalAppIntentProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/utilities/ExternalAppIntentProvider.java b/collect_app/src/main/java/org/odk/collect/android/utilities/ExternalAppIntentProvider.java index 1e7154e0025..603b1bc9542 100644 --- a/collect_app/src/main/java/org/odk/collect/android/utilities/ExternalAppIntentProvider.java +++ b/collect_app/src/main/java/org/odk/collect/android/utilities/ExternalAppIntentProvider.java @@ -21,7 +21,7 @@ public Intent getIntentToRunExternalApp(FormController formController, FormEntry String exSpec = appearance.substring(appearance.indexOf(Appearances.EX)); if (exSpec.contains(")")) { - exSpec = exSpec.substring(0, exSpec.indexOf(')') + 1); + exSpec = exSpec.substring(0, exSpec.lastIndexOf(')') + 1); } else if (exSpec.contains(" ")) { exSpec = exSpec.substring(0, exSpec.indexOf(' ')); } From 9922413825db18b5ad0fe289d1fddc536cb9f463 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 3 Jul 2024 15:53:57 +0100 Subject: [PATCH 714/750] Upgrade to JavaRosa release --- buildSrc/src/main/java/dependencies/Dependencies.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/dependencies/Dependencies.kt b/buildSrc/src/main/java/dependencies/Dependencies.kt index b2baf3a2f82..0e668302e5d 100644 --- a/buildSrc/src/main/java/dependencies/Dependencies.kt +++ b/buildSrc/src/main/java/dependencies/Dependencies.kt @@ -35,7 +35,7 @@ object Dependencies { const val rarepebble_colorpicker = "com.github.martin-stone:hsv-alpha-color-picker-android:3.1.0" const val commons_io = "commons-io:commons-io:2.5" // Commons 2.6+ introduce java.nio usage that we can't access until our minSdkVersion >= 26 (https://developer.android.com/reference/java/io/File#toPath()) const val opencsv = "com.opencsv:opencsv:5.9" - const val javarosa_online = "org.getodk:javarosa:4.4.0-SNAPSHOT" + const val javarosa_online = "org.getodk:javarosa:4.4.0" const val javarosa_local = "org.getodk:javarosa:local" const val javarosa = javarosa_online const val karumi_dexter = "com.karumi:dexter:6.2.3" From 644a37d74e07c7dd5f06fcd1274170537902a17b Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 8 Jul 2024 10:38:42 +0200 Subject: [PATCH 715/750] Load existing layers on fragment attach --- .../org/odk/collect/maps/layers/OfflineMapLayersPicker.kt | 6 ++++++ .../odk/collect/maps/layers/OfflineMapLayersViewModel.kt | 6 +----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 2e0a3f5a035..9e5534ecba6 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -1,5 +1,6 @@ package org.odk.collect.maps.layers +import android.content.Context import android.net.Uri import android.os.Bundle import android.view.LayoutInflater @@ -79,6 +80,11 @@ class OfflineMapLayersPicker( } } + override fun onAttach(context: Context) { + super.onAttach(context) + sharedViewModel.loadExistingLayers() + } + override fun onCreate(savedInstanceState: Bundle?) { childFragmentManager.fragmentFactory = FragmentFactoryBuilder() .forClass(OfflineMapLayersImporter::class) { diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt index e0ed74994a6..70e71523501 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -33,11 +33,7 @@ class OfflineMapLayersViewModel( private lateinit var tempLayersDir: File - init { - loadExistingLayers() - } - - private fun loadExistingLayers() { + fun loadExistingLayers() { trackableWorker.immediate( background = { val layers = referenceLayerRepository.getAll().sortedBy { it.name } From 89aa66df987e0e186fe4e4116a1ca238dd6768d2 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 8 Jul 2024 10:55:00 +0200 Subject: [PATCH 716/750] Fixed tests --- .../odk/collect/maps/layers/OfflineMapLayersImporterTest.kt | 6 +++++- .../odk/collect/maps/layers/OfflineMapLayersPickerTest.kt | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt index cf027a1f6ec..87f99aaa03a 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt @@ -102,7 +102,11 @@ class OfflineMapLayersImporterTest { @Test fun `the 'add layer' button is disabled during loading layers`() { - launchFragment() + val file = TempFiles.createTempFile("layer", MbtilesFile.FILE_EXTENSION) + + launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file.toUri()), it.requireContext()) + } onView(withId(org.odk.collect.maps.R.id.add_layer_button)).check(matches(not(isEnabled()))) scheduler.flush() diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index 1a4132092f3..e0e6f4e00f3 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -389,6 +389,7 @@ class OfflineMapLayersPickerTest { Interactions.clickOn(withText("layer1")) scenario.recreate() + scheduler.flush() onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check( matches( not(isChecked()) @@ -535,6 +536,7 @@ class OfflineMapLayersPickerTest { onView(withRecyclerView(R.id.layers).atPositionOnView(3, R.id.arrow)).perform(click()) scenario.recreate() + scheduler.flush() assertLayerExpanded(1) assertLayerCollapsed(2) From 8111e036b61ebcc844f312fc9cf81c79f9266911 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 4 Jul 2024 11:36:52 +0200 Subject: [PATCH 717/750] Improved listening setting changes in MapsPreferencesFragment --- .../screens/MapsPreferencesFragment.kt | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt index c9a3a4fe31f..88d9914a9f3 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt @@ -68,6 +68,13 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP super.onSettingChanged(key) if (key == ProjectKeys.KEY_REFERENCE_LAYER) { findPreference(ProjectKeys.KEY_REFERENCE_LAYER)!!.summary = getLayerName() + } else if (key == KEY_BASEMAP_SOURCE) { + val cftor = MapConfiguratorProvider.getConfigurator(settingsProvider.getUnprotectedSettings().getString(key)) + if (!cftor.isAvailable(requireContext())) { + cftor.showUnavailableMessage(requireContext()) + } else { + onBasemapSourceChanged(cftor) + } } } @@ -109,16 +116,6 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP basemapSourcePref.setIconSpaceReserved(false) onBasemapSourceChanged(MapConfiguratorProvider.getConfigurator()) - basemapSourcePref.setOnPreferenceChangeListener { _: Preference?, value: Any -> - val cftor = MapConfiguratorProvider.getConfigurator(value.toString()) - if (!cftor.isAvailable(requireContext())) { - cftor.showUnavailableMessage(requireContext()) - false - } else { - onBasemapSourceChanged(cftor) - true - } - } } private fun initLayersPref() { From a5bc2a92dbf37425b7f5503d404a21e137ae7035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Mon, 8 Jul 2024 09:47:58 -0700 Subject: [PATCH 718/750] Update translations for release --- strings/src/main/res/values-es/strings.xml | 3 +++ strings/src/main/res/values-zh-rTW/strings.xml | 12 ++++++------ strings/src/main/res/values-zh/strings.xml | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/strings/src/main/res/values-es/strings.xml b/strings/src/main/res/values-es/strings.xml index ff6ebda2a7b..797bc6d740b 100644 --- a/strings/src/main/res/values-es/strings.xml +++ b/strings/src/main/res/values-es/strings.xml @@ -117,6 +117,7 @@ Esta al final de \"%s\". Los formularios finalizados no pueden ser editados. Los formularios enviados no pueden ser editados. + Si necesita hacer cambios, \"Guardar como borrador\" hasta que esté listo para enviar. Aprende más Guardar como borrador Finalizar @@ -174,6 +175,7 @@ Esta aplicación no está. Por favor ingrese el dato manualmente. La aplicación externa no suministra la información esperada + Imprimir Iniciar Impresión La impresora solicitado no está instalada. Por favor, instale la impresora. Abrir URL @@ -925,6 +927,7 @@ + Este proyecto estaba previamente conectado a una cuenta de Google Drive. Se ha eliminado la compatibilidad con Google Drive. Puede configurar un servidor o conectar a una computadora. Aprende más 麥克風已在使用中。 - 無法將媒體附加到表單,但可以在 %1$s 獲取 + 無法將媒體附加到表單,但可以在 %1$s 取得 錄製已禁用。在 %s 中啟用 @@ -398,7 +398,7 @@ 選擇離線圖層 - 選擇要用於該項目中所有地圖的離線圖層。您可以通過從設備中選擇 MBTiles 文件來將選項添加到列表中,也可以從列表中刪除現有圖層。 + 選擇要用於該項目中所有地圖的離線圖層。您可從設備中選取 MBTiles 文件,添加選項到列表中,也可以從列表刪除現有圖層。 瞭解有關添加 MBTiles 的更多訊息。 @@ -447,11 +447,11 @@ 點將儲存在 %1$s 衛星:%1$s - 正在嘗試獲取位置,請稍候。 + 正在嘗試取得位置,請稍候.. 精度差,請稍候。 精度不可接受,請稍候。 提高準確性,請稍候。 - 正在獲取位置 + 正在取得位置 %1$sm @@ -550,7 +550,7 @@ 更新 下載選取項目 連接到伺服器 - 獲取 %1$s.\n\n表單 %2$s / %3$s 表單。 + 正在下載 %1$s.\n\n表單 %2$s / %3$s 表單.. %1$s. 載入媒體文件: %3$s 中的 %2$s 版本: 表單更新可用 diff --git a/strings/src/main/res/values-zh/strings.xml b/strings/src/main/res/values-zh/strings.xml index ccba4117299..6e8efc0d942 100644 --- a/strings/src/main/res/values-zh/strings.xml +++ b/strings/src/main/res/values-zh/strings.xml @@ -134,10 +134,10 @@ 继续编辑 储存表格? 您可以保存此表单并随时从草稿中存取它。 - \'此表格最后储存于\' EEE, MMM dd, yyyy \'at\' HH:mm\'。另存为草稿以保留变更。\' + \'此表格最后储存于\' EEE, MMM dd, yyyy\'at\'HH:mm\'。另存为草稿以保留变更。\' 继续编辑? 如果您丢弃表单,您将失去所有已做的更改。 - \'此表格最後儲存於\' EEE, MMM dd, yyyy HH:mm\',如果您放棄變更,將失去此後所做的更改。\' + \'此表格最后保存于 \'EEE, MMM dd, yyyy HH:mm\'。如果您放弃更改,您将失去此后所做的所有更改。\' 正在保存表格 正在验证答案… 正在收集数据… From 3dee292cae7f9147d79fefd72b772fc8b120fd35 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 17 Jul 2024 23:44:45 +0200 Subject: [PATCH 719/750] Fixed applying the masked appearance --- .../java/org/odk/collect/android/views/WidgetAnswerText.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/views/WidgetAnswerText.kt b/collect_app/src/main/java/org/odk/collect/android/views/WidgetAnswerText.kt index 0412b9452ca..e43a8bc9c04 100644 --- a/collect_app/src/main/java/org/odk/collect/android/views/WidgetAnswerText.kt +++ b/collect_app/src/main/java/org/odk/collect/android/views/WidgetAnswerText.kt @@ -59,11 +59,9 @@ class WidgetAnswerText(context: Context, attrs: AttributeSet?) : FrameLayout(con } }) if (isMasked) { - binding.editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + binding.editText.inputType = binding.editText.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD binding.editText.transformationMethod = PasswordTransformationMethod.getInstance() binding.textView.transformationMethod = PasswordTransformationMethod.getInstance() - } else { - binding.editText.inputType = InputType.TYPE_CLASS_TEXT } } From a9941403c22fcec2025cebb5955ba2e5a7136567 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 17 Jul 2024 23:45:55 +0200 Subject: [PATCH 720/750] Fixed tests --- .../collect/android/widgets/DecimalWidgetTest.java | 3 +-- .../android/widgets/ExDecimalWidgetTest.java | 3 +-- .../android/widgets/ExIntegerWidgetTest.java | 3 +-- .../collect/android/widgets/ExStringWidgetTest.java | 7 +++---- .../collect/android/widgets/IntegerWidgetTest.java | 3 +-- .../android/widgets/StringNumberWidgetTest.java | 13 +------------ .../collect/android/widgets/StringWidgetTest.java | 8 +++----- 7 files changed, 11 insertions(+), 29 deletions(-) diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/DecimalWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/DecimalWidgetTest.java index ea6bdc64f26..bbe404e7979 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/DecimalWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/DecimalWidgetTest.java @@ -9,7 +9,6 @@ import static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP; import android.text.InputType; -import android.text.method.SingleLineTransformationMethod; import androidx.annotation.NonNull; @@ -216,6 +215,6 @@ public void separatorsShouldBeAddedWhenEnabled() { public void verifyInputType() { DecimalWidget widget = getWidget(); assertThat(widget.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL)); - assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(SingleLineTransformationMethod.class)); + assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod(), equalTo(null)); } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/ExDecimalWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/ExDecimalWidgetTest.java index 52cb2f71780..1eb834a2c04 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/ExDecimalWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/ExDecimalWidgetTest.java @@ -9,7 +9,6 @@ import static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP; import android.text.InputType; -import android.text.method.SingleLineTransformationMethod; import androidx.annotation.NonNull; @@ -99,7 +98,7 @@ public void separatorsShouldBeAddedWhenEnabled() { public void verifyInputType() { ExDecimalWidget widget = getWidget(); assertThat(widget.binding.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL)); - assertThat(widget.binding.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(SingleLineTransformationMethod.class)); + assertThat(widget.binding.widgetAnswerText.getBinding().editText.getTransformationMethod(), equalTo(null)); assertThat(widget.binding.widgetAnswerText.getBinding().textView.getTransformationMethod(), equalTo(null)); } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/ExIntegerWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/ExIntegerWidgetTest.java index 546bd1136b2..c5f8d46ecfe 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/ExIntegerWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/ExIntegerWidgetTest.java @@ -7,7 +7,6 @@ import static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP; import android.text.InputType; -import android.text.method.SingleLineTransformationMethod; import androidx.annotation.NonNull; @@ -73,7 +72,7 @@ public void separatorsShouldBeAddedWhenEnabled() { public void verifyInputType() { ExIntegerWidget widget = getWidget(); assertThat(widget.binding.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED)); - assertThat(widget.binding.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(SingleLineTransformationMethod.class)); + assertThat(widget.binding.widgetAnswerText.getBinding().editText.getTransformationMethod(), equalTo(null)); assertThat(widget.binding.widgetAnswerText.getBinding().textView.getTransformationMethod(), equalTo(null)); } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/ExStringWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/ExStringWidgetTest.java index bebe2da4fde..d6c2cf68c67 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/ExStringWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/ExStringWidgetTest.java @@ -24,7 +24,6 @@ import android.text.InputType; import android.text.method.PasswordTransformationMethod; -import android.text.method.SingleLineTransformationMethod; /** * @author James Knight @@ -58,8 +57,8 @@ public void setUp() throws Exception { @Test public void verifyInputType() { ExStringWidget widget = getWidget(); - assertThat(widget.binding.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_TEXT)); - assertThat(widget.binding.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(SingleLineTransformationMethod.class)); + assertThat(widget.binding.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_MULTI_LINE)); + assertThat(widget.binding.widgetAnswerText.getBinding().editText.getTransformationMethod(), equalTo(null)); assertThat(widget.binding.widgetAnswerText.getBinding().textView.getTransformationMethod(), equalTo(null)); } @@ -67,7 +66,7 @@ public void verifyInputType() { public void verifyInputTypeWithMaskedAppearance() { when(formEntryPrompt.getAppearanceHint()).thenReturn(MASKED); ExStringWidget widget = getWidget(); - assertThat(widget.binding.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)); + assertThat(widget.binding.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_VARIATION_PASSWORD)); assertThat(widget.binding.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(PasswordTransformationMethod.class)); assertThat(widget.binding.widgetAnswerText.getBinding().textView.getTransformationMethod().getClass(), equalTo(PasswordTransformationMethod.class)); } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/IntegerWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/IntegerWidgetTest.java index 37282e96c4d..da5675d1643 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/IntegerWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/IntegerWidgetTest.java @@ -7,7 +7,6 @@ import static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP; import android.text.InputType; -import android.text.method.SingleLineTransformationMethod; import androidx.annotation.NonNull; @@ -60,6 +59,6 @@ public void separatorsShouldBeAddedWhenEnabled() { public void verifyInputType() { IntegerWidget widget = getWidget(); assertThat(widget.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED)); - assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(SingleLineTransformationMethod.class)); + assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod(), equalTo(null)); } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/StringNumberWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/StringNumberWidgetTest.java index caf9500376d..2d6abc64c55 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/StringNumberWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/StringNumberWidgetTest.java @@ -4,12 +4,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Mockito.when; -import static org.odk.collect.android.utilities.Appearances.MASKED; import static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP; import android.text.InputType; -import android.text.method.PasswordTransformationMethod; -import android.text.method.SingleLineTransformationMethod; import androidx.annotation.NonNull; @@ -60,14 +57,6 @@ public void separatorsShouldBeAddedWhenEnabled() { public void verifyInputType() { StringNumberWidget widget = getWidget(); assertThat(widget.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_NUMBER)); - assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(SingleLineTransformationMethod.class)); - } - - @Test - public void verifyInputTypeWithMaskedAppearance() { - when(formEntryPrompt.getAppearanceHint()).thenReturn(MASKED); - StringNumberWidget widget = getWidget(); - assertThat(widget.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_NUMBER)); - assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(PasswordTransformationMethod.class)); + assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod(), equalTo(null)); } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/StringWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/StringWidgetTest.java index bece4ed644b..8ca197e7f00 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/StringWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/StringWidgetTest.java @@ -10,7 +10,6 @@ import android.text.InputType; import android.text.method.PasswordTransformationMethod; -import android.text.method.SingleLineTransformationMethod; import androidx.annotation.NonNull; @@ -45,16 +44,15 @@ public StringData getNextAnswer() { @Test public void verifyInputType() { StringWidget widget = getWidget(); - assertThat(widget.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_TEXT)); - assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(SingleLineTransformationMethod.class)); - assertThat(widget.widgetAnswerText.getBinding().textView.getTransformationMethod(), equalTo(null)); + assertThat(widget.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_MULTI_LINE)); + assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod(), equalTo(null)); } @Test public void verifyInputTypeWithMaskedAppearance() { when(formEntryPrompt.getAppearanceHint()).thenReturn(MASKED); StringWidget widget = getWidget(); - assertThat(widget.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)); + assertThat(widget.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_VARIATION_PASSWORD)); assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod().getClass(), equalTo(PasswordTransformationMethod.class)); } From 3e86c79de3fdd656c23da5e54f2810731b201c11 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 18 Jul 2024 09:19:09 +0200 Subject: [PATCH 721/750] Added new tests --- .../collect/android/widgets/ExStringWidgetTest.java | 6 ++++++ .../odk/collect/android/widgets/StringWidgetTest.java | 6 ++++++ .../android/widgets/base/GeneralStringWidgetTest.java | 11 ----------- .../odk/collect/android/widgets/base/WidgetTest.java | 5 ++++- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/ExStringWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/ExStringWidgetTest.java index d6c2cf68c67..4dab30be6b1 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/ExStringWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/ExStringWidgetTest.java @@ -84,4 +84,10 @@ public void answersShouldBeMaskedIfMaskedAppearanceIsUsed() { assertThat(getSpyWidget().binding.widgetAnswerText.getBinding().editText.getTransformationMethod(), is(instanceOf(PasswordTransformationMethod.class))); assertThat(getSpyWidget().binding.widgetAnswerText.getBinding().textView.getTransformationMethod(), is(instanceOf(PasswordTransformationMethod.class))); } + + @Test + public void whenNumberOfRowsSpecifiedEditTextShouldHaveProperNumberOfMinLines() { + when(questionDef.getAdditionalAttribute(null, "rows")).thenReturn("5"); + assertThat(getWidget().binding.widgetAnswerText.getBinding().editText.getMinLines(), equalTo(5)); + } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/StringWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/StringWidgetTest.java index 8ca197e7f00..5580a45a1c0 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/StringWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/StringWidgetTest.java @@ -69,4 +69,10 @@ public void answersShouldBeMaskedIfMaskedAppearanceIsUsed() { assertThat(getSpyWidget().widgetAnswerText.getBinding().editText.getTransformationMethod(), is(instanceOf(PasswordTransformationMethod.class))); assertThat(getSpyWidget().widgetAnswerText.getBinding().textView.getTransformationMethod(), is(instanceOf(PasswordTransformationMethod.class))); } + + @Test + public void whenNumberOfRowsSpecifiedEditTextShouldHaveProperNumberOfMinLines() { + when(questionDef.getAdditionalAttribute(null, "rows")).thenReturn("5"); + assertThat(getWidget().widgetAnswerText.getBinding().editText.getMinLines(), equalTo(5)); + } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/base/GeneralStringWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/base/GeneralStringWidgetTest.java index 93408999016..c1e0e2affe6 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/base/GeneralStringWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/base/GeneralStringWidgetTest.java @@ -4,10 +4,8 @@ import android.view.View; -import org.javarosa.core.model.QuestionDef; import org.javarosa.core.model.data.IAnswerData; import org.junit.Test; -import org.mockito.Mock; import org.odk.collect.android.R; import org.odk.collect.android.support.WidgetTestActivity; import org.odk.collect.android.widgets.StringWidget; @@ -25,15 +23,6 @@ public abstract class GeneralStringWidgetTest extends QuestionWidgetTest { - @Mock - QuestionDef questionDef; - - @Override - public void setUp() throws Exception { - super.setUp(); - when(formEntryPrompt.getQuestion()).thenReturn(questionDef); - } - @Override public void callingClearShouldRemoveTheExistingAnswer() { super.callingClearShouldRemoveTheExistingAnswer(); diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/base/WidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/base/WidgetTest.java index 3058d239794..728a076d4b0 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/base/WidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/base/WidgetTest.java @@ -37,6 +37,9 @@ public abstract class WidgetTest { protected final SettingsProvider settingsProvider = TestSettingsProvider.getSettingsProvider(); + @Mock + protected QuestionDef questionDef; + @Before @OverridingMethodsMustInvokeSuper public void setUp() throws Exception { @@ -48,7 +51,7 @@ public void setUp() throws Exception { when(formEntryPrompt.getIndex()).thenReturn(mock(FormIndex.class)); when(formEntryPrompt.getIndex().toString()).thenReturn("0, 0"); when(formEntryPrompt.getFormElement()).thenReturn(formElement); - when(formEntryPrompt.getQuestion()).thenReturn(mock(QuestionDef.class)); + when(formEntryPrompt.getQuestion()).thenReturn(questionDef); } @Test From 5a83a15ad7b1fb6ac75335166eb8c9e22d568448 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 18 Jul 2024 09:34:54 +0100 Subject: [PATCH 722/750] Let Errors crash the app during form loading This means that OOM (and other Errors) will correctly end the app process and not end up being obscured in crash reporting. --- .../main/java/org/odk/collect/android/tasks/FormLoaderTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/tasks/FormLoaderTask.java b/collect_app/src/main/java/org/odk/collect/android/tasks/FormLoaderTask.java index f26c3f4daed..a7d10a533ff 100644 --- a/collect_app/src/main/java/org/odk/collect/android/tasks/FormLoaderTask.java +++ b/collect_app/src/main/java/org/odk/collect/android/tasks/FormLoaderTask.java @@ -197,7 +197,7 @@ protected FECWrapper doInBackground(Void... ignored) { } catch (StackOverflowError e) { Timber.e(e); errorMsg = getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.too_complex_form); - } catch (Exception | Error e) { + } catch (Exception e) { Timber.w(e); errorMsg = "An unknown error has occurred. Please ask your project leadership to email support@getodk.org with information about this form."; errorMsg += "\n\n" + e.getMessage(); From 5fb20f863951f9bf672ad0841c1c75f7adb55cb0 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 18 Jul 2024 16:27:40 +0200 Subject: [PATCH 723/750] Added failing tests --- .../android/widgets/QuestionWidget.java | 2 +- .../base/GeneralExStringWidgetTest.java | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/QuestionWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/QuestionWidget.java index 23a2e319960..b1cb2636d71 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/QuestionWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/QuestionWidget.java @@ -75,7 +75,7 @@ public abstract class QuestionWidget extends FrameLayout implements Widget { private final View guidanceTextLayout; private final View textLayout; private final TextView warningText; - protected final View errorLayout; + public final View errorLayout; protected final Settings settings; private AtomicBoolean expanded; protected final ThemeUtils themeUtils; diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/base/GeneralExStringWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/base/GeneralExStringWidgetTest.java index c85f61eac27..169c63f622e 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/base/GeneralExStringWidgetTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/base/GeneralExStringWidgetTest.java @@ -3,6 +3,7 @@ import static junit.framework.Assert.assertTrue; import android.view.View; +import android.widget.TextView; import org.javarosa.core.model.data.IAnswerData; import org.junit.Test; @@ -11,6 +12,7 @@ import org.odk.collect.android.widgets.ExStringWidget; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.when; @@ -64,6 +66,26 @@ public void widgetShouldBeRegisteredForContextMenu() { assertThat(viewsRegisterForContextMenu.get(1).getId(), is(widget.getId())); } + @Test + public void errorDisappearsOnSetData() { + ExStringWidget widget = getWidget(); + widget.displayError("blah"); + widget.setData("answer"); + + assertThat(widget.errorLayout.getVisibility(), equalTo(TextView.GONE)); + assertThat(widget.getBackground(), equalTo(null)); + } + + @Test + public void errorDisappearsOnAddingAnswerManuallyViaTheTextField() { + ExStringWidget widget = getWidget(); + widget.displayError("blah"); + widget.binding.widgetAnswerText.getBinding().editText.setText("answer"); + + assertThat(widget.errorLayout.getVisibility(), equalTo(TextView.GONE)); + assertThat(widget.getBackground(), equalTo(null)); + } + @Test public abstract void verifyInputType(); } From 6649c6227a44a032a11d1271606245fd66e369d8 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 18 Jul 2024 16:27:59 +0200 Subject: [PATCH 724/750] Fixed hiding error in external widgets --- .../java/org/odk/collect/android/widgets/ExStringWidget.java | 1 + 1 file changed, 1 insertion(+) diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/ExStringWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/ExStringWidget.java index 5ce32044ddc..7f5ee7206f2 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/ExStringWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/ExStringWidget.java @@ -236,6 +236,7 @@ private void onException(String toastText) { @Override public void hideError() { + super.hideError(); binding.widgetAnswerText.setError(null); } From 184247b64e5fbc76744e64928faaa5cd62efbd78 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 18 Jul 2024 21:19:51 +0200 Subject: [PATCH 725/750] Fixed passing the SavepointsRepositoryProvider to viewModelFactory --- .../odk/collect/android/activities/FormFillingActivity.java | 2 +- .../collect/android/formhierarchy/FormHierarchyActivity.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) 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 a4852cb6740..edb78d0df28 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 @@ -429,7 +429,7 @@ public void onCreate(Bundle savedInstanceState) { autoSendSettingsProvider, formsRepositoryProvider, instancesRepositoryProvider, - new SavepointsRepositoryProvider(this, storagePathProvider), + savepointsRepositoryProvider, new QRCodeCreatorImpl(), new HtmlPrinter(), instancesDataService 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 77d4f5ff444..d601e58f94d 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 @@ -59,7 +59,6 @@ import org.odk.collect.android.javarosawrapper.FormController; import org.odk.collect.android.javarosawrapper.JavaRosaFormController; import org.odk.collect.android.projects.ProjectsDataService; -import org.odk.collect.android.storage.StoragePathProvider; import org.odk.collect.android.utilities.ApplicationConstants; import org.odk.collect.android.utilities.FormEntryPromptUtils; import org.odk.collect.android.utilities.FormsRepositoryProvider; @@ -194,7 +193,7 @@ public class FormHierarchyActivity extends LocalizedActivity implements DeleteRe public FormsRepositoryProvider formsRepositoryProvider; @Inject - public StoragePathProvider storagePathProvider; + public SavepointsRepositoryProvider savepointsRepositoryProvider; @Inject public InstancesDataService instancesDataService; @@ -232,7 +231,7 @@ public void onCreate(Bundle savedInstanceState) { autoSendSettingsProvider, formsRepositoryProvider, instancesRepositoryProvider, - new SavepointsRepositoryProvider(this, storagePathProvider), + savepointsRepositoryProvider, new QRCodeCreatorImpl(), new HtmlPrinter(), instancesDataService From 1dad86a4d0dcb54dd901717562d5a45ed7677682 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 18 Jul 2024 22:52:12 +0200 Subject: [PATCH 726/750] Fixed passing dependencies to BackgroundLocationHelper --- .../activities/FormEntryViewModelFactory.kt | 8 ++-- .../BackgroundLocationHelper.java | 37 +++++++++++++------ 2 files changed, 30 insertions(+), 15 deletions(-) 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 fd9c6007091..d75fa1be656 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 @@ -129,10 +129,10 @@ class FormEntryViewModelFactory( fusedLocationClient, BackgroundLocationHelper( permissionsProvider, - settingsProvider.getUnprotectedSettings() - ) { - formSessionRepository.get(sessionId).value?.formController - } + settingsProvider.getUnprotectedSettings(), + formSessionRepository, + sessionId + ) ) BackgroundLocationViewModel(locationManager) diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/backgroundlocation/BackgroundLocationHelper.java b/collect_app/src/main/java/org/odk/collect/android/formentry/backgroundlocation/BackgroundLocationHelper.java index 9f41a88125b..f6b49ef9790 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/backgroundlocation/BackgroundLocationHelper.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/backgroundlocation/BackgroundLocationHelper.java @@ -6,6 +6,8 @@ import org.odk.collect.android.activities.FormFillingActivity; import org.odk.collect.android.application.Collect; +import org.odk.collect.android.formentry.FormSession; +import org.odk.collect.android.formentry.FormSessionRepository; import org.odk.collect.android.formentry.audit.AuditConfig; import org.odk.collect.android.formentry.audit.AuditEvent; import org.odk.collect.android.javarosawrapper.FormController; @@ -13,7 +15,7 @@ import org.odk.collect.permissions.PermissionsProvider; import org.odk.collect.shared.settings.Settings; -import java.util.function.Supplier; +import javax.annotation.Nullable; /** * Wrapper on resources needed by {@link BackgroundLocationManager} to make testing easier. @@ -29,12 +31,19 @@ public class BackgroundLocationHelper { private final PermissionsProvider permissionsProvider; private final Settings generalSettings; - private final Supplier formControllerProvider; - - public BackgroundLocationHelper(PermissionsProvider permissionsProvider, Settings generalSettings, Supplier formControllerProvider) { + private final FormSessionRepository formSessionRepository; + private final String sessionId; + + public BackgroundLocationHelper( + PermissionsProvider permissionsProvider, + Settings generalSettings, + FormSessionRepository formSessionRepository, + String sessionId + ) { this.permissionsProvider = permissionsProvider; this.generalSettings = generalSettings; - this.formControllerProvider = formControllerProvider; + this.formSessionRepository = formSessionRepository; + this.sessionId = sessionId; } boolean isAndroidLocationPermissionGranted() { @@ -53,7 +62,7 @@ boolean arePlayServicesAvailable() { * @return true if the global form controller has been initialized. */ boolean isCurrentFormSet() { - return formControllerProvider.get() != null; + return getFormController() != null; } /** @@ -62,7 +71,7 @@ boolean isCurrentFormSet() { * Precondition: the global form controller has been initialized. */ boolean currentFormCollectsBackgroundLocation() { - return formControllerProvider.get().currentFormCollectsBackgroundLocation(); + return getFormController().currentFormCollectsBackgroundLocation(); } /** @@ -72,7 +81,7 @@ boolean currentFormCollectsBackgroundLocation() { * Precondition: the global form controller has been initialized. */ boolean currentFormAuditsLocation() { - return formControllerProvider.get().currentFormAuditsLocation(); + return getFormController().currentFormAuditsLocation(); } /** @@ -81,7 +90,7 @@ boolean currentFormAuditsLocation() { * Precondition: the global form controller has been initialized. */ AuditConfig getCurrentFormAuditConfig() { - return formControllerProvider.get().getSubmissionMetadata().auditConfig; + return getFormController().getSubmissionMetadata().auditConfig; } /** @@ -90,7 +99,7 @@ AuditConfig getCurrentFormAuditConfig() { * Precondition: the global form controller has been initialized. */ void logAuditEvent(AuditEvent.AuditEventType eventType) { - formControllerProvider.get().getAuditEventLogger().logEvent(eventType, false, System.currentTimeMillis()); + getFormController().getAuditEventLogger().logEvent(eventType, false, System.currentTimeMillis()); } /** @@ -99,6 +108,12 @@ void logAuditEvent(AuditEvent.AuditEventType eventType) { * Precondition: the global form controller has been initialized. */ void provideLocationToAuditLogger(Location location) { - formControllerProvider.get().getAuditEventLogger().addLocation(location); + getFormController().getAuditEventLogger().addLocation(location); + } + + @Nullable + private FormController getFormController() { + FormSession formSession = formSessionRepository.get(sessionId).getValue(); + return formSession == null ? null : formSession.getFormController(); } } From 187d498132335589743e22d67746905fb536c057 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 12 Aug 2024 11:01:37 +0200 Subject: [PATCH 727/750] Added a failing test --- .../OpenRosaResponseParserImplTest.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/collect_app/src/test/java/org/odk/collect/android/openrosa/OpenRosaResponseParserImplTest.kt b/collect_app/src/test/java/org/odk/collect/android/openrosa/OpenRosaResponseParserImplTest.kt index fef35c8ead3..17359bc975c 100644 --- a/collect_app/src/test/java/org/odk/collect/android/openrosa/OpenRosaResponseParserImplTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/openrosa/OpenRosaResponseParserImplTest.kt @@ -71,4 +71,29 @@ class OpenRosaResponseParserImplTest { val formList = OpenRosaResponseParserImpl().parseManifest(Document()) assertThat(formList, equalTo(null)) } + + @Test + fun `parseManifest() sanitizes media file names`() { + val response = StringBuilder() + .appendLine("") + .appendLine("") + .appendLine("") + .appendLine("/../badgers.csv") + .appendLine("blah") + .appendLine("http://funk.appspot.com/binaryData?blobKey=%3A477e3") + .appendLine("") + .appendLine("") + .toString() + + val doc = StringReader(response).use { reader -> + val parser = KXmlParser() + parser.setInput(reader) + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true) + Document().also { it.parse(parser) } + } + + val mediaFiles = OpenRosaResponseParserImpl().parseManifest(doc)!! + assertThat(mediaFiles.size, equalTo(1)) + assertThat(mediaFiles[0].filename, equalTo("badgers.csv")) + } } From 5dc2baaa8ebe3fda092c7b036ee77bfb47d8d391 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 12 Aug 2024 11:06:29 +0200 Subject: [PATCH 728/750] Sanitize media file names --- .../odk/collect/android/openrosa/OpenRosaResponseParserImpl.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/collect_app/src/main/java/org/odk/collect/android/openrosa/OpenRosaResponseParserImpl.kt b/collect_app/src/main/java/org/odk/collect/android/openrosa/OpenRosaResponseParserImpl.kt index 862a61dc63c..3c0089753d4 100644 --- a/collect_app/src/main/java/org/odk/collect/android/openrosa/OpenRosaResponseParserImpl.kt +++ b/collect_app/src/main/java/org/odk/collect/android/openrosa/OpenRosaResponseParserImpl.kt @@ -179,6 +179,9 @@ class OpenRosaResponseParserImpl : OpenRosaResponseParser { if (filename != null && filename.isEmpty()) { filename = null } + if (filename != null) { + filename = filename.substringAfterLast("/", filename) + } } "hash" -> { hash = XFormParser.getXMLText(child, true) From bc6f01949bf88f6606ab89b742046a528bf086b8 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 12 Aug 2024 12:02:02 +0200 Subject: [PATCH 729/750] Removed support for InstanceProvider#update --- .../android/external/InstanceProvider.java | 66 +------------------ 1 file changed, 1 insertion(+), 65 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/external/InstanceProvider.java b/collect_app/src/main/java/org/odk/collect/android/external/InstanceProvider.java index 87288c3cf40..bbfd28f5e94 100644 --- a/collect_app/src/main/java/org/odk/collect/android/external/InstanceProvider.java +++ b/collect_app/src/main/java/org/odk/collect/android/external/InstanceProvider.java @@ -14,9 +14,7 @@ package org.odk.collect.android.external; -import static org.odk.collect.android.database.DatabaseObjectMapper.getInstanceFromCurrentCursorPosition; import static org.odk.collect.android.database.DatabaseObjectMapper.getInstanceFromValues; -import static org.odk.collect.android.database.DatabaseObjectMapper.getValuesFromInstance; import static org.odk.collect.android.database.instances.DatabaseInstanceColumns._ID; import static org.odk.collect.android.external.InstancesContract.CONTENT_ITEM_TYPE; import static org.odk.collect.android.external.InstancesContract.CONTENT_TYPE; @@ -37,12 +35,10 @@ import org.odk.collect.android.injection.DaggerUtils; import org.odk.collect.android.instancemanagement.InstanceDeleter; import org.odk.collect.android.storage.StoragePathProvider; -import org.odk.collect.android.storage.StorageSubdirectory; import org.odk.collect.android.utilities.ContentUriHelper; import org.odk.collect.android.utilities.FormsRepositoryProvider; import org.odk.collect.android.utilities.InstancesRepositoryProvider; import org.odk.collect.forms.instances.Instance; -import org.odk.collect.forms.instances.InstancesRepository; import org.odk.collect.projects.ProjectsRepository; import org.odk.collect.settings.SettingsProvider; @@ -197,67 +193,7 @@ public int delete(@NonNull Uri uri, String where, String[] whereArgs) { @Override public int update(@NonNull Uri uri, ContentValues values, String where, String[] whereArgs) { - DaggerUtils.getComponent(getContext()).inject(this); - - String projectId = getProjectId(uri); - logServerEvent(projectId, AnalyticsEvents.INSTANCE_PROVIDER_UPDATE); - - InstancesRepository instancesRepository = instancesRepositoryProvider.get(projectId); - String instancesPath = storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES, projectId); - - int count; - - switch (URI_MATCHER.match(uri)) { - case INSTANCES: - try (Cursor cursor = dbQuery(projectId, null, where, whereArgs, null)) { - while (cursor.moveToNext()) { - Instance instance = getInstanceFromCurrentCursorPosition(cursor, instancesPath); - ContentValues existingValues = getValuesFromInstance(instance, instancesPath); - - existingValues.putAll(values); - instancesRepository.save(getInstanceFromValues(existingValues)); - } - - count = cursor.getCount(); - } - - break; - - case INSTANCE_ID: - long instanceId = ContentUriHelper.getIdFromUri(uri); - if (whereArgs == null || whereArgs.length == 0) { - Instance instance = instancesRepository.get(instanceId); - ContentValues existingValues = getValuesFromInstance(instance, instancesPath); - - existingValues.putAll(values); - instancesRepository.save(getInstanceFromValues(existingValues)); - count = 1; - } else { - try (Cursor cursor = dbQuery(projectId, new String[]{_ID}, where, whereArgs, null)) { - while (cursor.moveToNext()) { - if (cursor.getLong(cursor.getColumnIndex(_ID)) == instanceId) { - Instance instance = getInstanceFromCurrentCursorPosition(cursor, instancesPath); - ContentValues existingValues = getValuesFromInstance(instance, instancesPath); - - existingValues.putAll(values); - instancesRepository.save(getInstanceFromValues(existingValues)); - break; - } - } - } - - count = 1; - } - - break; - - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - - getContext().getContentResolver().notifyChange(uri, null); - - return count; + return 0; } private String getProjectId(@NonNull Uri uri) { From ab81aae20c28ab05ad191400b3d860a6d747d182 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 12 Aug 2024 13:51:38 +0200 Subject: [PATCH 730/750] Fixed tests --- .../external/InstanceProviderTest.java | 93 +++---------------- 1 file changed, 14 insertions(+), 79 deletions(-) diff --git a/collect_app/src/test/java/org/odk/collect/android/external/InstanceProviderTest.java b/collect_app/src/test/java/org/odk/collect/android/external/InstanceProviderTest.java index 6b4a41c4fc4..f9393f4f881 100644 --- a/collect_app/src/test/java/org/odk/collect/android/external/InstanceProviderTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/external/InstanceProviderTest.java @@ -27,7 +27,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.odk.collect.android.database.instances.DatabaseInstanceColumns.DELETED_DATE; @@ -92,7 +91,7 @@ public void insert_returnsInstanceUri() { } @Test - public void update_updatesInstance_andReturns1() { + public void update_doesNotUpdateInstance_andReturns0() { ContentValues values = getContentValues("/blah", "External app form", "external_app_form", "1"); long originalStatusChangeDate = 0L; @@ -103,78 +102,14 @@ public void update_updatesInstance_andReturns1() { updateValues.put(STATUS, STATUS_COMPLETE); int updatedCount = contentResolver.update(instanceUri, updateValues, null, null); - assertThat(updatedCount, is(1)); - try (Cursor cursor = contentResolver.query(instanceUri, null, null, null)) { - assertThat(cursor.getCount(), is(1)); - - cursor.moveToNext(); - assertThat(cursor.getString(cursor.getColumnIndex(STATUS)), is(STATUS_COMPLETE)); - assertThat(cursor.getLong(cursor.getColumnIndex(LAST_STATUS_CHANGE_DATE)), is(not(originalStatusChangeDate))); - assertThat(cursor.getLong(cursor.getColumnIndex(LAST_STATUS_CHANGE_DATE)), is(notNullValue())); - } - } - - @Test - public void update_whenDeletedDateIsIncluded_doesNotUpdateStatusChangeDate() { - ContentValues values = getContentValues("/blah", "External app form", "external_app_form", "1"); - - long originalStatusChangeDate = 0L; - values.put(LAST_STATUS_CHANGE_DATE, originalStatusChangeDate); - Uri instanceUri = contentResolver.insert(getUri(firstProjectId), values); - - ContentValues updateValues = new ContentValues(); - updateValues.put(DELETED_DATE, 123L); - - contentResolver.update(instanceUri, updateValues, null, null); + assertThat(updatedCount, is(0)); try (Cursor cursor = contentResolver.query(instanceUri, null, null, null)) { assertThat(cursor.getCount(), is(1)); cursor.moveToNext(); + assertThat(cursor.getString(cursor.getColumnIndex(STATUS)), is(STATUS_INCOMPLETE)); assertThat(cursor.getLong(cursor.getColumnIndex(LAST_STATUS_CHANGE_DATE)), is(originalStatusChangeDate)); - } - } - - @Test - public void update_withSelection_onlyUpdatesMatchingInstance() { - addInstanceToDb(firstProjectId, "/blah1", "Instance 1"); - addInstanceToDb(firstProjectId, "/blah2", "Instance 2"); - - ContentValues updateValues = new ContentValues(); - updateValues.put(STATUS, STATUS_COMPLETE); - contentResolver.update(getUri(firstProjectId), updateValues, INSTANCE_FILE_PATH + "=?", new String[]{"/blah2"}); - - try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null, null)) { - assertThat(cursor.getCount(), is(2)); - - while (cursor.moveToNext()) { - if (cursor.getString(cursor.getColumnIndex(INSTANCE_FILE_PATH)).equals("/blah2")) { - assertThat(cursor.getString(cursor.getColumnIndex(STATUS)), is(STATUS_COMPLETE)); - } else { - assertThat(cursor.getString(cursor.getColumnIndex(STATUS)), is(STATUS_INCOMPLETE)); - } - } - } - } - - /** - * It's not clear when this is used. A hypothetical might be updating an instance but wanting - * that to be a no-op if it has already been soft deleted. - */ - @Test - public void update_withInstanceUri_andSelection_doesNotUpdateInstanceThatDoesNotMatchSelection() { - Uri uri = addInstanceToDb(firstProjectId, "/blah1", "Instance 1"); - addInstanceToDb(firstProjectId, "/blah1", "Instance 2"); - - ContentValues updateValues = new ContentValues(); - updateValues.put(STATUS, STATUS_COMPLETE); - contentResolver.update(uri, updateValues, DISPLAY_NAME + "=?", new String[]{"Instance 2"}); - - try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null, null)) { - assertThat(cursor.getCount(), is(2)); - - while (cursor.moveToNext()) { - assertThat(cursor.getString(cursor.getColumnIndex(STATUS)), is(STATUS_INCOMPLETE)); - } + assertThat(cursor.getLong(cursor.getColumnIndex(LAST_STATUS_CHANGE_DATE)), is(0L)); } } @@ -200,11 +135,11 @@ public void delete_deletesInstanceDir() { @Test public void delete_whenStatusIsSubmitted_deletesFilesButSoftDeletesInstance() { File instanceFile = createInstanceDirAndFile(firstProjectId); - Uri uri = addInstanceToDb(firstProjectId, instanceFile.getAbsolutePath(), "Instance 1"); - ContentValues updateValues = new ContentValues(); - updateValues.put(STATUS, STATUS_SUBMITTED); - contentResolver.update(uri, updateValues, null, null); + ContentValues values = getContentValues(instanceFile.getAbsolutePath(), "Instance 1", "external_app_form", "1"); + values.put(STATUS, STATUS_SUBMITTED); + + Uri uri = contentResolver.insert(getUri(firstProjectId), values); contentResolver.delete(uri, null, null); assertThat(instanceFile.getParentFile().exists(), is(false)); @@ -220,13 +155,13 @@ public void delete_whenStatusIsSubmitted_deletesFilesButSoftDeletesInstance() { @Test public void delete_whenStatusIsSubmitted_clearsGeometryFields() { File instanceFile = createInstanceDirAndFile(firstProjectId); - Uri uri = addInstanceToDb(firstProjectId, instanceFile.getAbsolutePath(), "Instance 1"); - ContentValues updateValues = new ContentValues(); - updateValues.put(STATUS, STATUS_SUBMITTED); - updateValues.put(GEOMETRY, "something"); - updateValues.put(GEOMETRY_TYPE, "something else"); - contentResolver.update(uri, updateValues, null, null); + ContentValues values = getContentValues(instanceFile.getAbsolutePath(), "Instance 1", "external_app_form", "1"); + values.put(STATUS, STATUS_SUBMITTED); + values.put(GEOMETRY, "something"); + values.put(GEOMETRY_TYPE, "something else"); + + Uri uri = contentResolver.insert(getUri(firstProjectId), values); contentResolver.delete(uri, null, null); assertThat(instanceFile.getParentFile().exists(), is(false)); From 553e46fbbc7f8a024343e3a62dafd35a0d3336bc Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 13 Aug 2024 11:31:34 +0200 Subject: [PATCH 731/750] Improve sanitizing file names --- .../odk/collect/android/openrosa/OpenRosaResponseParserImpl.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/openrosa/OpenRosaResponseParserImpl.kt b/collect_app/src/main/java/org/odk/collect/android/openrosa/OpenRosaResponseParserImpl.kt index 3c0089753d4..2d2dfc12d9a 100644 --- a/collect_app/src/main/java/org/odk/collect/android/openrosa/OpenRosaResponseParserImpl.kt +++ b/collect_app/src/main/java/org/odk/collect/android/openrosa/OpenRosaResponseParserImpl.kt @@ -6,6 +6,7 @@ import org.kxml2.kdom.Element import org.odk.collect.forms.FormListItem import org.odk.collect.forms.MediaFile import org.odk.collect.shared.strings.StringUtils.isBlank +import java.io.File class OpenRosaResponseParserImpl : OpenRosaResponseParser { @@ -180,7 +181,7 @@ class OpenRosaResponseParserImpl : OpenRosaResponseParser { filename = null } if (filename != null) { - filename = filename.substringAfterLast("/", filename) + filename = File(filename).name } } "hash" -> { From af77951fde56ab0bf91903643ba1fccd4ad2b01a Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 13 Aug 2024 15:48:50 +0200 Subject: [PATCH 732/750] Removed unused analytics event --- .../java/org/odk/collect/android/analytics/AnalyticsEvents.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index e103f7b8432..45f296d7bd6 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -85,8 +85,6 @@ object AnalyticsEvents { const val INSTANCE_PROVIDER_INSERT = "InstanceProviderInsert" - const val INSTANCE_PROVIDER_UPDATE = "InstanceProviderUpdate" - const val INSTANCE_PROVIDER_DELETE = "InstanceProviderDelete" /** From 77f7d88efed1cccf414e5883c8381f3734470db0 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 13 Aug 2024 17:09:40 +0100 Subject: [PATCH 733/750] Guard against incorrect paths --- shared/src/main/java/org/odk/collect/shared/PathUtils.kt | 8 +++++++- .../src/test/java/org/odk/collect/shared/PathUtilsTest.kt | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/shared/src/main/java/org/odk/collect/shared/PathUtils.kt b/shared/src/main/java/org/odk/collect/shared/PathUtils.kt index 71e04c51e7b..6c68afdf929 100644 --- a/shared/src/main/java/org/odk/collect/shared/PathUtils.kt +++ b/shared/src/main/java/org/odk/collect/shared/PathUtils.kt @@ -11,7 +11,13 @@ object PathUtils { @JvmStatic fun getAbsoluteFilePath(dirPath: String, filePath: String): String { - return if (filePath.startsWith(dirPath)) filePath else dirPath + File.separator + filePath + val absolutePath = if (filePath.startsWith(dirPath)) filePath else dirPath + File.separator + filePath + + if (File(absolutePath).canonicalPath.startsWith(dirPath)) { + return absolutePath + } else { + throw SecurityException() + } } // https://stackoverflow.com/questions/2679699/what-characters-allowed-in-file-names-on-android diff --git a/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt b/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt index 8e33688dc75..34e2463db42 100644 --- a/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt +++ b/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt @@ -24,6 +24,11 @@ class PathUtilsTest { assertThat(path, equalTo("/root/dir/file")) } + @Test(expected = SecurityException::class) + fun `getAbsoluteFilePath() throws SecurityException when filePath is outside the dirPath`() { + PathUtils.getAbsoluteFilePath("/root/dir", "../tmp/file") + } + @Test fun `getRelativeFilePath() returns filePath with dirPath removed`() { val path = PathUtils.getRelativeFilePath("/root/dir", "/root/dir/file") From a3130e258cc99271d919db2a6200aeefb4ac900b Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 13 Aug 2024 18:00:32 +0100 Subject: [PATCH 734/750] Don't allow inserting instances with custom submission URI --- .../android/external/InstanceProvider.java | 5 +++++ .../android/external/InstanceProviderTest.java | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/collect_app/src/main/java/org/odk/collect/android/external/InstanceProvider.java b/collect_app/src/main/java/org/odk/collect/android/external/InstanceProvider.java index bbfd28f5e94..c3ef6e07ec9 100644 --- a/collect_app/src/main/java/org/odk/collect/android/external/InstanceProvider.java +++ b/collect_app/src/main/java/org/odk/collect/android/external/InstanceProvider.java @@ -31,6 +31,7 @@ import org.odk.collect.android.analytics.AnalyticsEvents; import org.odk.collect.android.analytics.AnalyticsUtils; import org.odk.collect.android.dao.CursorLoaderFactory; +import org.odk.collect.android.database.instances.DatabaseInstanceColumns; import org.odk.collect.android.database.instances.DatabaseInstancesRepository; import org.odk.collect.android.injection.DaggerUtils; import org.odk.collect.android.instancemanagement.InstanceDeleter; @@ -132,6 +133,10 @@ public Uri insert(@NonNull Uri uri, ContentValues initialValues) { throw new IllegalArgumentException("Unknown URI " + uri); } + if (initialValues.containsKey(DatabaseInstanceColumns.SUBMISSION_URI)) { + throw new SecurityException(); + } + Instance newInstance = instancesRepositoryProvider.get(projectId).save(getInstanceFromValues(initialValues)); return getUri(projectId, newInstance.getDbId()); } diff --git a/collect_app/src/test/java/org/odk/collect/android/external/InstanceProviderTest.java b/collect_app/src/test/java/org/odk/collect/android/external/InstanceProviderTest.java index f9393f4f881..f2c1db59297 100644 --- a/collect_app/src/test/java/org/odk/collect/android/external/InstanceProviderTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/external/InstanceProviderTest.java @@ -13,6 +13,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.odk.collect.android.database.forms.DatabaseFormColumns; +import org.odk.collect.android.database.instances.DatabaseInstanceColumns; import org.odk.collect.android.injection.DaggerUtils; import org.odk.collect.android.storage.StoragePathProvider; import org.odk.collect.android.storage.StorageSubdirectory; @@ -29,6 +30,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.fail; import static org.odk.collect.android.database.instances.DatabaseInstanceColumns.DELETED_DATE; import static org.odk.collect.android.database.instances.DatabaseInstanceColumns.DISPLAY_NAME; import static org.odk.collect.android.database.instances.DatabaseInstanceColumns.GEOMETRY; @@ -80,6 +82,19 @@ public void insert_addsInstance() { } } + @Test + public void insert_withSubmissionUri_throwsException() { + ContentValues values = getContentValues("/blah", "External app form", "external_app_form", "1"); + values.put(DatabaseInstanceColumns.SUBMISSION_URI, "https://blah.com/submission"); + + try { + contentResolver.insert(getUri(firstProjectId), values); + fail(); + } catch (SecurityException e) { + // Expected + } + } + @Test public void insert_returnsInstanceUri() { ContentValues values = getContentValues("/blah", "External app form", "external_app_form", "1"); From 2ae728ce8614d5c5be4ae65aa015e479df97927b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Tue, 13 Aug 2024 10:14:25 -0700 Subject: [PATCH 735/750] Update JavaRosa to 4.4.1 for CSV parse improvements --- buildSrc/src/main/java/dependencies/Dependencies.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/dependencies/Dependencies.kt b/buildSrc/src/main/java/dependencies/Dependencies.kt index 0e668302e5d..10a172b6172 100644 --- a/buildSrc/src/main/java/dependencies/Dependencies.kt +++ b/buildSrc/src/main/java/dependencies/Dependencies.kt @@ -35,7 +35,7 @@ object Dependencies { const val rarepebble_colorpicker = "com.github.martin-stone:hsv-alpha-color-picker-android:3.1.0" const val commons_io = "commons-io:commons-io:2.5" // Commons 2.6+ introduce java.nio usage that we can't access until our minSdkVersion >= 26 (https://developer.android.com/reference/java/io/File#toPath()) const val opencsv = "com.opencsv:opencsv:5.9" - const val javarosa_online = "org.getodk:javarosa:4.4.0" + const val javarosa_online = "org.getodk:javarosa:4.4.1" const val javarosa_local = "org.getodk:javarosa:local" const val javarosa = javarosa_online const val karumi_dexter = "com.karumi:dexter:6.2.3" From 318997138f82d6d69bbd05ae90fe7cc9d6550d85 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 14 Aug 2024 09:47:04 +0100 Subject: [PATCH 736/750] Remove FormsProvider#update --- .../android/external/FormsProvider.java | 50 +------------ .../android/external/FormsProviderTest.java | 72 ++++++------------- 2 files changed, 22 insertions(+), 100 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/external/FormsProvider.java b/collect_app/src/main/java/org/odk/collect/android/external/FormsProvider.java index 3f52998a244..3dadc98f792 100644 --- a/collect_app/src/main/java/org/odk/collect/android/external/FormsProvider.java +++ b/collect_app/src/main/java/org/odk/collect/android/external/FormsProvider.java @@ -15,9 +15,7 @@ package org.odk.collect.android.external; import static android.provider.BaseColumns._ID; -import static org.odk.collect.android.database.DatabaseObjectMapper.getFormFromCurrentCursorPosition; import static org.odk.collect.android.database.DatabaseObjectMapper.getFormFromValues; -import static org.odk.collect.android.database.DatabaseObjectMapper.getValuesFromForm; import static org.odk.collect.android.database.forms.DatabaseFormColumns.AUTO_DELETE; import static org.odk.collect.android.database.forms.DatabaseFormColumns.AUTO_SEND; import static org.odk.collect.android.database.forms.DatabaseFormColumns.BASE64_RSA_PUBLIC_KEY; @@ -248,53 +246,7 @@ public int delete(@NonNull Uri uri, String where, String[] whereArgs) { @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { - deferDaggerInit(); - - String projectId = getProjectId(uri); - logServerEvent(projectId, AnalyticsEvents.FORMS_PROVIDER_UPDATE); - - FormsRepository formsRepository = getFormsRepository(projectId); - String formsPath = storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, projectId); - String cachePath = storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, projectId); - - int count; - - switch (URI_MATCHER.match(uri)) { - case FORMS: - try (Cursor cursor = databaseQuery(projectId, null, where, whereArgs, null, null, null)) { - while (cursor.moveToNext()) { - Form form = getFormFromCurrentCursorPosition(cursor, formsPath, cachePath); - ContentValues existingValues = getValuesFromForm(form, formsPath); - existingValues.putAll(values); - - formsRepository.save(getFormFromValues(existingValues, formsPath, cachePath)); - } - - count = cursor.getCount(); - } - break; - - case FORM_ID: - Form form = formsRepository.get(ContentUriHelper.getIdFromUri(uri)); - if (form != null) { - ContentValues existingValues = getValuesFromForm(form, formsPath); - existingValues.putAll(values); - - formsRepository.save(getFormFromValues(existingValues, formsPath, cachePath)); - count = 1; - } else { - count = 0; - } - - break; - - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - - getContext().getContentResolver().notifyChange(uri, null); - - return count; + return 0; } @NotNull diff --git a/collect_app/src/test/java/org/odk/collect/android/external/FormsProviderTest.java b/collect_app/src/test/java/org/odk/collect/android/external/FormsProviderTest.java index 490311974b6..d1cad0a830b 100644 --- a/collect_app/src/test/java/org/odk/collect/android/external/FormsProviderTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/external/FormsProviderTest.java @@ -1,5 +1,23 @@ package org.odk.collect.android.external; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isOneOf; +import static org.hamcrest.Matchers.notNullValue; +import static org.odk.collect.android.database.forms.DatabaseFormColumns.DATE; +import static org.odk.collect.android.database.forms.DatabaseFormColumns.DISPLAY_NAME; +import static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_FILE_PATH; +import static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_MEDIA_PATH; +import static org.odk.collect.android.database.forms.DatabaseFormColumns.JRCACHE_FILE_PATH; +import static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_FORM_ID; +import static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_VERSION; +import static org.odk.collect.android.database.forms.DatabaseFormColumns.LANGUAGE; +import static org.odk.collect.android.database.forms.DatabaseFormColumns.MD5_HASH; +import static org.odk.collect.android.external.FormsContract.CONTENT_ITEM_TYPE; +import static org.odk.collect.android.external.FormsContract.CONTENT_TYPE; +import static org.odk.collect.android.external.FormsContract.getUri; + import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; @@ -25,23 +43,6 @@ import java.io.File; import java.io.IOException; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isOneOf; -import static org.hamcrest.Matchers.notNullValue; -import static org.odk.collect.android.database.forms.DatabaseFormColumns.DATE; -import static org.odk.collect.android.database.forms.DatabaseFormColumns.DISPLAY_NAME; -import static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_FILE_PATH; -import static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_MEDIA_PATH; -import static org.odk.collect.android.database.forms.DatabaseFormColumns.JRCACHE_FILE_PATH; -import static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_FORM_ID; -import static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_VERSION; -import static org.odk.collect.android.database.forms.DatabaseFormColumns.LANGUAGE; -import static org.odk.collect.android.database.forms.DatabaseFormColumns.MD5_HASH; -import static org.odk.collect.android.external.FormsContract.CONTENT_ITEM_TYPE; -import static org.odk.collect.android.external.FormsContract.CONTENT_TYPE; -import static org.odk.collect.android.external.FormsContract.getUri; - @RunWith(AndroidJUnit4.class) public class FormsProviderTest { @@ -101,53 +102,22 @@ public void insert_returnsFormUri() { } @Test - public void update_updatesForm_andReturns1() { + public void update_doesNotUpdateForms_andReturns0() { Uri formUri = addFormsToDirAndDb(firstProjectId, "external_app_form", "External app form", "1"); ContentValues contentValues = new ContentValues(); contentValues.put(LANGUAGE, "English"); int updateCount = contentResolver.update(formUri, contentValues, null, null); - assertThat(updateCount, is(1)); + assertThat(updateCount, is(0)); try (Cursor cursor = contentResolver.query(formUri, null, null, null)) { assertThat(cursor.getCount(), is(1)); cursor.moveToNext(); - assertThat(cursor.getString(cursor.getColumnIndex(LANGUAGE)), is("English")); + assertThat(cursor.getString(cursor.getColumnIndex(LANGUAGE)), equalTo(null)); } } - @Test - public void update_withSelection_onlyUpdatesMatchingForms() { - addFormsToDirAndDb(firstProjectId, "form1", "Matching form", "1"); - addFormsToDirAndDb(firstProjectId, "form2", "Not matching form", "1"); - addFormsToDirAndDb(firstProjectId, "form3", "Matching form", "1"); - - ContentValues contentValues = new ContentValues(); - contentValues.put(LANGUAGE, "English"); - - contentResolver.update(getUri(firstProjectId), contentValues, DISPLAY_NAME + "=?", new String[]{"Matching form"}); - try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null)) { - assertThat(cursor.getCount(), is(3)); - - cursor.moveToNext(); - if (cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)).equals("Matching form")) { - assertThat(cursor.getString(cursor.getColumnIndex(LANGUAGE)), is("English")); - } else { - assertThat(cursor.isNull(cursor.getColumnIndex(LANGUAGE)), is(true)); - } - } - } - - @Test - public void update_whenFormDoesNotExist_returns0() { - ContentValues contentValues = new ContentValues(); - contentValues.put(LANGUAGE, "English"); - - int updatedCount = contentResolver.update(Uri.withAppendedPath(getUri(firstProjectId), String.valueOf(1)), contentValues, null, null); - assertThat(updatedCount, is(0)); - } - @Test public void delete_deletesForm() { Uri formUri = addFormsToDirAndDb(firstProjectId, "form1", "Matching form", "1"); From 9f93bcb1ea65e0fed3927176ae86411178ba0357 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 14 Aug 2024 11:01:15 +0100 Subject: [PATCH 737/750] Remove FormsProvider#insert --- .../android/external/FormsProvider.java | 18 +----- .../android/external/FormsProviderTest.java | 64 +++++++------------ 2 files changed, 25 insertions(+), 57 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/external/FormsProvider.java b/collect_app/src/main/java/org/odk/collect/android/external/FormsProvider.java index 3dadc98f792..9d6fab6eb5d 100644 --- a/collect_app/src/main/java/org/odk/collect/android/external/FormsProvider.java +++ b/collect_app/src/main/java/org/odk/collect/android/external/FormsProvider.java @@ -15,7 +15,6 @@ package org.odk.collect.android.external; import static android.provider.BaseColumns._ID; -import static org.odk.collect.android.database.DatabaseObjectMapper.getFormFromValues; import static org.odk.collect.android.database.forms.DatabaseFormColumns.AUTO_DELETE; import static org.odk.collect.android.database.forms.DatabaseFormColumns.AUTO_SEND; import static org.odk.collect.android.database.forms.DatabaseFormColumns.BASE64_RSA_PUBLIC_KEY; @@ -50,11 +49,9 @@ import org.odk.collect.android.injection.DaggerUtils; import org.odk.collect.android.itemsets.FastExternalItemsetsRepository; import org.odk.collect.android.storage.StoragePathProvider; -import org.odk.collect.android.storage.StorageSubdirectory; import org.odk.collect.android.utilities.ContentUriHelper; import org.odk.collect.android.utilities.FormsRepositoryProvider; import org.odk.collect.android.utilities.InstancesRepositoryProvider; -import org.odk.collect.forms.Form; import org.odk.collect.forms.FormsRepository; import org.odk.collect.forms.instances.InstancesRepository; import org.odk.collect.projects.ProjectsRepository; @@ -187,20 +184,7 @@ public String getType(@NonNull Uri uri) { @Override public synchronized Uri insert(@NonNull Uri uri, ContentValues initialValues) { - deferDaggerInit(); - - // Validate the requested uri - if (URI_MATCHER.match(uri) != FORMS) { - throw new IllegalArgumentException("Unknown URI " + uri); - } - - String projectId = getProjectId(uri); - logServerEvent(projectId, AnalyticsEvents.FORMS_PROVIDER_INSERT); - - String formsPath = storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, projectId); - String cachePath = storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, projectId); - Form form = getFormsRepository(projectId).save(getFormFromValues(initialValues, formsPath, cachePath)); - return FormsContract.getUri(projectId, form.getDbId()); + return null; } /** diff --git a/collect_app/src/test/java/org/odk/collect/android/external/FormsProviderTest.java b/collect_app/src/test/java/org/odk/collect/android/external/FormsProviderTest.java index d1cad0a830b..1a7c94521e0 100644 --- a/collect_app/src/test/java/org/odk/collect/android/external/FormsProviderTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/external/FormsProviderTest.java @@ -4,8 +4,6 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isOneOf; -import static org.hamcrest.Matchers.notNullValue; -import static org.odk.collect.android.database.forms.DatabaseFormColumns.DATE; import static org.odk.collect.android.database.forms.DatabaseFormColumns.DISPLAY_NAME; import static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_FILE_PATH; import static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_MEDIA_PATH; @@ -13,7 +11,6 @@ import static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_FORM_ID; import static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_VERSION; import static org.odk.collect.android.database.forms.DatabaseFormColumns.LANGUAGE; -import static org.odk.collect.android.database.forms.DatabaseFormColumns.MD5_HASH; import static org.odk.collect.android.external.FormsContract.CONTENT_ITEM_TYPE; import static org.odk.collect.android.external.FormsContract.CONTENT_TYPE; import static org.odk.collect.android.external.FormsContract.getUri; @@ -32,10 +29,14 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.odk.collect.android.injection.DaggerUtils; +import org.odk.collect.android.injection.config.AppDependencyComponent; import org.odk.collect.android.storage.StoragePathProvider; import org.odk.collect.android.storage.StorageSubdirectory; import org.odk.collect.android.support.CollectHelpers; import org.odk.collect.android.utilities.FileUtils; +import org.odk.collect.android.utilities.FormsRepositoryProvider; +import org.odk.collect.forms.Form; +import org.odk.collect.forms.FormsRepository; import org.odk.collect.formstest.FormUtils; import org.odk.collect.projects.Project; import org.odk.collect.shared.strings.Md5; @@ -49,55 +50,31 @@ public class FormsProviderTest { private ContentResolver contentResolver; private StoragePathProvider storagePathProvider; private String firstProjectId; + private AppDependencyComponent component; @Before public void setup() { Context context = ApplicationProvider.getApplicationContext(); - storagePathProvider = DaggerUtils.getComponent(context).storagePathProvider(); + component = DaggerUtils.getComponent(context); + storagePathProvider = component.storagePathProvider(); firstProjectId = CollectHelpers.createDemoProject(); contentResolver = context.getContentResolver(); } @Test - public void insert_addsForm() { + public void insert_doesNotInsertForms_andReturnsNull() { String formId = "external_app_form"; String formVersion = "1"; String formName = "External app form"; File formFile = addFormToFormsDir(firstProjectId, formId, formVersion, formName); - String md5Hash = Md5.getMd5Hash(formFile); ContentValues values = getContentValues(formId, formVersion, formName, formFile); - contentResolver.insert(getUri(firstProjectId), values); + Uri uri = contentResolver.insert(getUri(firstProjectId), values); + assertThat(uri, equalTo(null)); try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null, null)) { - assertThat(cursor.getCount(), is(1)); - - cursor.moveToNext(); - assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(formName)); - assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(formId)); - assertThat(cursor.getString(cursor.getColumnIndex(JR_VERSION)), is(formVersion)); - assertThat(cursor.getString(cursor.getColumnIndex(FORM_FILE_PATH)), is(formFile.getName())); - - assertThat(cursor.getString(cursor.getColumnIndex(DATE)), is(notNullValue())); - assertThat(cursor.getString(cursor.getColumnIndex(MD5_HASH)), is(md5Hash)); - assertThat(cursor.getString(cursor.getColumnIndex(JRCACHE_FILE_PATH)), is(md5Hash + ".formdef")); - assertThat(cursor.getString(cursor.getColumnIndex(FORM_MEDIA_PATH)), is(mediaPathForFormFile(formFile))); - } - } - - @Test - public void insert_returnsFormUri() { - String formId = "external_app_form"; - String formVersion = "1"; - String formName = "External app form"; - File formFile = addFormToFormsDir(firstProjectId, formId, formVersion, formName); - - ContentValues values = getContentValues(formId, formVersion, formName, formFile); - Uri newFormUri = contentResolver.insert(getUri(firstProjectId), values); - - try (Cursor cursor = contentResolver.query(newFormUri, null, null, null, null)) { - assertThat(cursor.getCount(), is(1)); + assertThat(cursor.getCount(), is(0)); } } @@ -259,8 +236,19 @@ public void getType_returnsFormAndAllFormsTypes() { private Uri addFormsToDirAndDb(String projectId, String id, String name, String version) { File formFile = addFormToFormsDir(projectId, id, version, name); - ContentValues values = getContentValues(id, version, name, formFile); - return contentResolver.insert(getUri(projectId), values); + + FormsRepositoryProvider formsRepositoryProvider = component.formsRepositoryProvider(); + FormsRepository formsRepository = formsRepositoryProvider.get(projectId); + Form form = formsRepository.save( + new Form.Builder() + .formId(id) + .displayName(name) + .version(version) + .formFilePath(formFile.getAbsolutePath()) + .build() + ); + + return FormsContract.getUri(projectId, form.getDbId()); } @NotNull @@ -312,10 +300,6 @@ private void createExtraFormFiles(String projectId, File formFile, String md5Has } } - private String mediaPathForFormFile(File newFile) { - return newFile.getName().substring(0, newFile.getName().lastIndexOf(".")) + "-media"; - } - @NotNull private String getFormsDirPath(String projectId) { return storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, projectId) + File.separator; From 37c9604d92b5e5948bcfbd417e6cde3ed396dd60 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 14 Aug 2024 14:40:26 +0100 Subject: [PATCH 738/750] Add test for instance upload action --- .../external/InstanceUploadActionTest.kt | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt index 1eebf01a831..11ea68091cb 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt @@ -1,40 +1,68 @@ package org.odk.collect.android.feature.external +import android.content.Context import android.content.Intent +import android.provider.BaseColumns._ID +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain import org.junit.runner.RunWith import org.odk.collect.android.external.InstancesContract -import org.odk.collect.android.instancemanagement.send.InstanceUploaderActivity +import org.odk.collect.android.support.TestDependencies +import org.odk.collect.android.support.pages.FormEntryPage import org.odk.collect.android.support.pages.OkDialog import org.odk.collect.android.support.rules.CollectTestRule import org.odk.collect.android.support.rules.TestRuleChain +import org.odk.collect.android.utilities.ApplicationConstants @RunWith(AndroidJUnit4::class) class InstanceUploadActionTest { - val collectTestRule = CollectTestRule() + private val rule = CollectTestRule(useDemoProject = false) + private val context = ApplicationProvider.getApplicationContext() + private val testDependencies = TestDependencies() @get:Rule - val rule: RuleChain = TestRuleChain.chain() - .around(collectTestRule) + val chain: RuleChain = TestRuleChain.chain(testDependencies) + .around(rule) @Test - fun whenInstanceDoesNotExist_showsError() { - val instanceIds = longArrayOf(11) - instanceUploadAction(instanceIds) + fun canUploadInstanceToDifferentServer() { + rule.startAtFirstLaunch().clickTryCollect() + .copyForm("one-question.xml") + .startBlankForm("One Question") + .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer("what is your age", "34")) - OkDialog() - .assertOnPage() - .assertText(org.odk.collect.strings.R.string.no_forms_uploaded) + val instanceId = + context.contentResolver.query(InstancesContract.getUri("DEMO"), null, null, null, null) + .use { + it!!.moveToFirst() + it.getLong(it.getColumnIndex(_ID)) + } + + val intent = Intent("org.odk.collect.android.INSTANCE_UPLOAD") + intent.type = InstancesContract.CONTENT_TYPE + intent.putExtra(ApplicationConstants.BundleKeys.URL, testDependencies.server.url) + intent.putExtra("instances", longArrayOf(instanceId)) + + rule.launch(intent, OkDialog()) + .assertTextInDialog("One Question - Success") + assertThat(testDependencies.server.submissions.size, equalTo(1)) } - private fun instanceUploadAction(instanceIds: LongArray) { + @Test + fun whenInstanceDoesNotExist_showsError() { + rule.startAtFirstLaunch().clickTryCollect() + val intent = Intent("org.odk.collect.android.INSTANCE_UPLOAD") intent.type = InstancesContract.CONTENT_TYPE - intent.putExtra("instances", instanceIds) - collectTestRule.launch(intent) + intent.putExtra("instances", longArrayOf(11)) + + rule.launch(intent, OkDialog()) + .assertText(org.odk.collect.strings.R.string.no_forms_uploaded) } } From 7b6984fac3b6cb2593244ccef4898ac1d7539ca8 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 14 Aug 2024 14:43:46 +0100 Subject: [PATCH 739/750] Add analytics for instance upload action --- .../android/analytics/AnalyticsEvents.kt | 5 ++++ .../android/tasks/InstanceUploaderTask.java | 27 +++++++++++-------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index 45f296d7bd6..b9a5030ad37 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -99,4 +99,9 @@ object AnalyticsEvents { const val DELETE_SAVED_FORM_FEW = "DeleteSavedFormFew" // < 10 const val DELETE_SAVED_FORM_TENS = "DeleteSavedFormTens" // >= 10 const val DELETE_SAVED_FORM_HUNDREDS = "DeleteSavedFormHundreds" // >= 100 + + /** + * Tracks how often the INSTANCE_UPLOAD action is used with a custom server URL + */ + const val INSTANCE_UPLOAD_CUSTOM_SERVER = "InstanceUploadCustomServer" } diff --git a/collect_app/src/main/java/org/odk/collect/android/tasks/InstanceUploaderTask.java b/collect_app/src/main/java/org/odk/collect/android/tasks/InstanceUploaderTask.java index 9f80bec7d8b..f33aaad70a5 100644 --- a/collect_app/src/main/java/org/odk/collect/android/tasks/InstanceUploaderTask.java +++ b/collect_app/src/main/java/org/odk/collect/android/tasks/InstanceUploaderTask.java @@ -14,21 +14,28 @@ package org.odk.collect.android.tasks; +import static org.odk.collect.android.analytics.AnalyticsEvents.SUBMISSION; +import static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString; + +import android.net.Uri; +import android.os.AsyncTask; + import org.odk.collect.analytics.Analytics; +import org.odk.collect.android.analytics.AnalyticsEvents; import org.odk.collect.android.application.Collect; import org.odk.collect.android.instancemanagement.InstanceDeleter; import org.odk.collect.android.instancemanagement.InstancesDataService; import org.odk.collect.android.listeners.InstanceUploaderListener; +import org.odk.collect.android.openrosa.OpenRosaHttpInterface; import org.odk.collect.android.projects.ProjectsDataService; +import org.odk.collect.android.upload.FormUploadAuthRequestedException; +import org.odk.collect.android.upload.FormUploadException; +import org.odk.collect.android.upload.InstanceServerUploader; import org.odk.collect.android.utilities.InstanceAutoDeleteChecker; import org.odk.collect.android.utilities.InstancesRepositoryProvider; +import org.odk.collect.android.utilities.WebCredentialsUtils; import org.odk.collect.forms.FormsRepository; import org.odk.collect.forms.instances.Instance; -import org.odk.collect.android.openrosa.OpenRosaHttpInterface; -import org.odk.collect.android.upload.InstanceServerUploader; -import org.odk.collect.android.upload.FormUploadAuthRequestedException; -import org.odk.collect.android.upload.FormUploadException; -import org.odk.collect.android.utilities.WebCredentialsUtils; import org.odk.collect.forms.instances.InstancesRepository; import org.odk.collect.metadata.PropertyManager; import org.odk.collect.settings.SettingsProvider; @@ -41,12 +48,6 @@ import javax.inject.Inject; -import static org.odk.collect.android.analytics.AnalyticsEvents.SUBMISSION; -import static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString; - -import android.net.Uri; -import android.os.AsyncTask; - /** * Background task for uploading completed forms. * @@ -100,6 +101,10 @@ public Outcome doInBackground(Long... instanceIdsToUpload) { publishProgress(i + 1, instancesToUpload.size()); + if (completeDestinationUrl != null) { + Analytics.log(AnalyticsEvents.INSTANCE_UPLOAD_CUSTOM_SERVER); + } + try { String destinationUrl = uploader.getUrlToSubmitTo(instance, deviceId, completeDestinationUrl, null); String customMessage = uploader.uploadOneSubmission(instance, destinationUrl); From a7f9312ea6b3bde1a777e2555fc2687814ebb63b Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 14 Aug 2024 16:37:26 +0100 Subject: [PATCH 740/750] Make test name more specific --- .../android/feature/external/InstanceUploadActionTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt index 11ea68091cb..4dd11ab419b 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt @@ -31,7 +31,7 @@ class InstanceUploadActionTest { .around(rule) @Test - fun canUploadInstanceToDifferentServer() { + fun whenIntentIncludesURLExtra_instancesAreUploadedToThatURL() { rule.startAtFirstLaunch().clickTryCollect() .copyForm("one-question.xml") .startBlankForm("One Question") From ba5ed931aaff18d1ba68c4e7e9943552f495a454 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 15 Aug 2024 08:25:15 +0100 Subject: [PATCH 741/750] Remove unneeded extra setup --- .../android/feature/external/InstanceUploadActionTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt index 4dd11ab419b..6e729b6ead6 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt @@ -22,7 +22,7 @@ import org.odk.collect.android.utilities.ApplicationConstants @RunWith(AndroidJUnit4::class) class InstanceUploadActionTest { - private val rule = CollectTestRule(useDemoProject = false) + private val rule = CollectTestRule() private val context = ApplicationProvider.getApplicationContext() private val testDependencies = TestDependencies() @@ -32,7 +32,7 @@ class InstanceUploadActionTest { @Test fun whenIntentIncludesURLExtra_instancesAreUploadedToThatURL() { - rule.startAtFirstLaunch().clickTryCollect() + rule.startAtMainMenu() .copyForm("one-question.xml") .startBlankForm("One Question") .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer("what is your age", "34")) @@ -56,7 +56,7 @@ class InstanceUploadActionTest { @Test fun whenInstanceDoesNotExist_showsError() { - rule.startAtFirstLaunch().clickTryCollect() + rule.startAtMainMenu() val intent = Intent("org.odk.collect.android.INSTANCE_UPLOAD") intent.type = InstancesContract.CONTENT_TYPE From d414c0ae2fd88da5659c68fe273eddf8074c7f13 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 20 Aug 2024 08:59:10 +0100 Subject: [PATCH 742/750] Add message to PathUtils security exception --- shared/src/main/java/org/odk/collect/shared/PathUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/main/java/org/odk/collect/shared/PathUtils.kt b/shared/src/main/java/org/odk/collect/shared/PathUtils.kt index 6c68afdf929..927796a4ed8 100644 --- a/shared/src/main/java/org/odk/collect/shared/PathUtils.kt +++ b/shared/src/main/java/org/odk/collect/shared/PathUtils.kt @@ -16,7 +16,7 @@ object PathUtils { if (File(absolutePath).canonicalPath.startsWith(dirPath)) { return absolutePath } else { - throw SecurityException() + throw SecurityException("Invalid path: $absolutePath") } } From 8a9ccd758419ea41ca2cae259d64a5b12685af11 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 20 Aug 2024 16:43:09 +0100 Subject: [PATCH 743/750] Support non canonical directory paths for getAbsoluteFilePath --- .../main/java/org/odk/collect/shared/PathUtils.kt | 2 +- .../main/java/org/odk/collect/shared/TempFiles.kt | 2 +- .../java/org/odk/collect/shared/PathUtilsTest.kt | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/shared/src/main/java/org/odk/collect/shared/PathUtils.kt b/shared/src/main/java/org/odk/collect/shared/PathUtils.kt index 927796a4ed8..c97b2e38243 100644 --- a/shared/src/main/java/org/odk/collect/shared/PathUtils.kt +++ b/shared/src/main/java/org/odk/collect/shared/PathUtils.kt @@ -13,7 +13,7 @@ object PathUtils { fun getAbsoluteFilePath(dirPath: String, filePath: String): String { val absolutePath = if (filePath.startsWith(dirPath)) filePath else dirPath + File.separator + filePath - if (File(absolutePath).canonicalPath.startsWith(dirPath)) { + if (File(absolutePath).canonicalPath.startsWith(File(dirPath).canonicalPath)) { return absolutePath } else { throw SecurityException("Invalid path: $absolutePath") diff --git a/shared/src/main/java/org/odk/collect/shared/TempFiles.kt b/shared/src/main/java/org/odk/collect/shared/TempFiles.kt index f5b8fb422e1..f58c52b628f 100644 --- a/shared/src/main/java/org/odk/collect/shared/TempFiles.kt +++ b/shared/src/main/java/org/odk/collect/shared/TempFiles.kt @@ -84,7 +84,7 @@ object TempFiles { } private fun getTempDir(): File { - val tmpDir = File(System.getProperty("java.io.tmpdir", "."), " org.odk.collect.shared.TempFiles") + val tmpDir = File(System.getProperty("java.io.tmpdir", "."), "org.odk.collect.shared.TempFiles") if (!tmpDir.exists()) { tmpDir.mkdir() } diff --git a/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt b/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt index 34e2463db42..006b48f9472 100644 --- a/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt +++ b/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt @@ -3,6 +3,7 @@ package org.odk.collect.shared import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo import org.junit.Test +import java.io.File class PathUtilsTest { @@ -29,6 +30,17 @@ class PathUtilsTest { PathUtils.getAbsoluteFilePath("/root/dir", "../tmp/file") } + @Test + fun `getAbsoluteFilePath() works when dirPath is not canonical`() { + val tempDir = TempFiles.createTempDir() + val nonCanonicalPath = + tempDir.canonicalPath + File.separator + ".." + File.separator + tempDir.name + assertThat(File(nonCanonicalPath).canonicalPath, equalTo(tempDir.canonicalPath)) + + val path = PathUtils.getAbsoluteFilePath(nonCanonicalPath, "file") + assertThat(path, equalTo(nonCanonicalPath + File.separator + "file")) + } + @Test fun `getRelativeFilePath() returns filePath with dirPath removed`() { val path = PathUtils.getRelativeFilePath("/root/dir", "/root/dir/file") From 6482ae26ae1de1f0707dfb840a2251f262128840 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 20 Aug 2024 16:51:08 +0100 Subject: [PATCH 744/750] Improve PathUtils crash message --- shared/src/main/java/org/odk/collect/shared/PathUtils.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/shared/src/main/java/org/odk/collect/shared/PathUtils.kt b/shared/src/main/java/org/odk/collect/shared/PathUtils.kt index c97b2e38243..66de93ac3d1 100644 --- a/shared/src/main/java/org/odk/collect/shared/PathUtils.kt +++ b/shared/src/main/java/org/odk/collect/shared/PathUtils.kt @@ -11,12 +11,13 @@ object PathUtils { @JvmStatic fun getAbsoluteFilePath(dirPath: String, filePath: String): String { - val absolutePath = if (filePath.startsWith(dirPath)) filePath else dirPath + File.separator + filePath + val absolutePath = + if (filePath.startsWith(dirPath)) filePath else dirPath + File.separator + filePath if (File(absolutePath).canonicalPath.startsWith(File(dirPath).canonicalPath)) { return absolutePath } else { - throw SecurityException("Invalid path: $absolutePath") + throw SecurityException("Contact support@getodk.org. Attempt to access file outside of Collect directory: $absolutePath") } } From 8f05e2813bf24faf00606ea79104fd7b4a533d4f Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 21 Aug 2024 12:07:19 +0200 Subject: [PATCH 745/750] Moved getAbsoluteFilePath to androidshared --- .../collect/androidshared/utils/PathUtils.kt | 17 +++++ .../android/database/DatabaseObjectMapper.kt | 2 +- .../DatabaseSavepointsRepository.kt | 5 +- .../fastexternalitemset/ItemsetDbAdapter.java | 4 +- .../download/ServerFormDownloaderTest.java | 2 +- .../java/org/odk/collect/shared/PathUtils.kt | 14 ---- .../org/odk/collect/shared/PathUtilsTest.kt | 68 +++++++++---------- 7 files changed, 59 insertions(+), 53 deletions(-) create mode 100644 androidshared/src/main/java/org/odk/collect/androidshared/utils/PathUtils.kt diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/utils/PathUtils.kt b/androidshared/src/main/java/org/odk/collect/androidshared/utils/PathUtils.kt new file mode 100644 index 00000000000..1f20205bf03 --- /dev/null +++ b/androidshared/src/main/java/org/odk/collect/androidshared/utils/PathUtils.kt @@ -0,0 +1,17 @@ +package org.odk.collect.androidshared.utils + +import java.io.File + +object PathUtils { + @JvmStatic + fun getAbsoluteFilePath(dirPath: String, filePath: String): String { + val absolutePath = + if (filePath.startsWith(dirPath)) filePath else dirPath + File.separator + filePath + + if (File(absolutePath).canonicalPath.startsWith(File(dirPath).canonicalPath)) { + return absolutePath + } else { + throw SecurityException("Contact support@getodk.org. Attempt to access file outside of Collect directory: $absolutePath") + } + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/database/DatabaseObjectMapper.kt b/collect_app/src/main/java/org/odk/collect/android/database/DatabaseObjectMapper.kt index 5183d7ca90e..8015bcae310 100644 --- a/collect_app/src/main/java/org/odk/collect/android/database/DatabaseObjectMapper.kt +++ b/collect_app/src/main/java/org/odk/collect/android/database/DatabaseObjectMapper.kt @@ -5,9 +5,9 @@ import android.database.Cursor import android.provider.BaseColumns import org.odk.collect.android.database.forms.DatabaseFormColumns import org.odk.collect.android.database.instances.DatabaseInstanceColumns +import org.odk.collect.androidshared.utils.PathUtils.getAbsoluteFilePath import org.odk.collect.forms.Form import org.odk.collect.forms.instances.Instance -import org.odk.collect.shared.PathUtils.getAbsoluteFilePath import org.odk.collect.shared.PathUtils.getRelativeFilePath import java.lang.Boolean diff --git a/collect_app/src/main/java/org/odk/collect/android/database/savepoints/DatabaseSavepointsRepository.kt b/collect_app/src/main/java/org/odk/collect/android/database/savepoints/DatabaseSavepointsRepository.kt index f71be767631..e134be02cb1 100644 --- a/collect_app/src/main/java/org/odk/collect/android/database/savepoints/DatabaseSavepointsRepository.kt +++ b/collect_app/src/main/java/org/odk/collect/android/database/savepoints/DatabaseSavepointsRepository.kt @@ -10,6 +10,7 @@ import org.odk.collect.android.database.DatabaseConstants.SAVEPOINTS_DATABASE_NA import org.odk.collect.android.database.DatabaseConstants.SAVEPOINTS_DATABASE_VERSION import org.odk.collect.android.database.savepoints.DatabaseSavepointsColumns.FORM_DB_ID import org.odk.collect.android.database.savepoints.DatabaseSavepointsColumns.INSTANCE_DB_ID +import org.odk.collect.androidshared.utils.PathUtils.getAbsoluteFilePath import org.odk.collect.forms.savepoints.Savepoint import org.odk.collect.forms.savepoints.SavepointsRepository import org.odk.collect.shared.PathUtils @@ -129,11 +130,11 @@ class DatabaseSavepointsRepository( return Savepoint( cursor.getLong(formDbIdColumnIndex), if (cursor.isNull(instanceDbIdColumnIndex)) null else cursor.getLong(instanceDbIdColumnIndex), - PathUtils.getAbsoluteFilePath( + getAbsoluteFilePath( cachePath, cursor.getString(savepointFilePathColumnIndex) ), - PathUtils.getAbsoluteFilePath( + getAbsoluteFilePath( instancesPath, cursor.getString(instanceDirPathColumnIndex) ) diff --git a/collect_app/src/main/java/org/odk/collect/android/fastexternalitemset/ItemsetDbAdapter.java b/collect_app/src/main/java/org/odk/collect/android/fastexternalitemset/ItemsetDbAdapter.java index a45736fa8b4..7393d4f004f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/fastexternalitemset/ItemsetDbAdapter.java +++ b/collect_app/src/main/java/org/odk/collect/android/fastexternalitemset/ItemsetDbAdapter.java @@ -1,5 +1,7 @@ package org.odk.collect.android.fastexternalitemset; +import static org.odk.collect.androidshared.utils.PathUtils.getAbsoluteFilePath; + import android.content.ContentValues; import android.database.Cursor; import android.database.SQLException; @@ -189,7 +191,7 @@ public void delete(String path) { if (c != null) { if (c.getCount() == 1) { c.moveToFirst(); - String table = getMd5FromString(PathUtils.getAbsoluteFilePath(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS), c.getString(c.getColumnIndex(KEY_PATH)))); + String table = getMd5FromString(getAbsoluteFilePath(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS), c.getString(c.getColumnIndex(KEY_PATH)))); db.execSQL("DROP TABLE IF EXISTS " + DATABASE_TABLE + table); } c.close(); diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/download/ServerFormDownloaderTest.java b/collect_app/src/test/java/org/odk/collect/android/formmanagement/download/ServerFormDownloaderTest.java index eb01f7f8fba..c3b41b4baf3 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formmanagement/download/ServerFormDownloaderTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/download/ServerFormDownloaderTest.java @@ -12,9 +12,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.odk.collect.android.utilities.FileUtils.read; +import static org.odk.collect.androidshared.utils.PathUtils.getAbsoluteFilePath; import static org.odk.collect.formstest.FormUtils.buildForm; import static org.odk.collect.formstest.FormUtils.createXFormBody; -import static org.odk.collect.shared.PathUtils.getAbsoluteFilePath; import static java.util.Arrays.asList; import static java.util.Arrays.stream; import static java.util.stream.Collectors.toList; diff --git a/shared/src/main/java/org/odk/collect/shared/PathUtils.kt b/shared/src/main/java/org/odk/collect/shared/PathUtils.kt index 66de93ac3d1..1f4ba227cc8 100644 --- a/shared/src/main/java/org/odk/collect/shared/PathUtils.kt +++ b/shared/src/main/java/org/odk/collect/shared/PathUtils.kt @@ -1,7 +1,5 @@ package org.odk.collect.shared -import java.io.File - object PathUtils { @JvmStatic @@ -9,18 +7,6 @@ object PathUtils { return if (filePath.startsWith(dirPath)) filePath.substring(dirPath.length + 1) else filePath } - @JvmStatic - fun getAbsoluteFilePath(dirPath: String, filePath: String): String { - val absolutePath = - if (filePath.startsWith(dirPath)) filePath else dirPath + File.separator + filePath - - if (File(absolutePath).canonicalPath.startsWith(File(dirPath).canonicalPath)) { - return absolutePath - } else { - throw SecurityException("Contact support@getodk.org. Attempt to access file outside of Collect directory: $absolutePath") - } - } - // https://stackoverflow.com/questions/2679699/what-characters-allowed-in-file-names-on-android @JvmStatic fun getPathSafeFileName(fileName: String) = fileName.replace("[\"*/:<>?\\\\|]".toRegex(), "_") diff --git a/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt b/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt index 006b48f9472..b9f66174409 100644 --- a/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt +++ b/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt @@ -3,43 +3,43 @@ package org.odk.collect.shared import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo import org.junit.Test -import java.io.File +// import java.io.File class PathUtilsTest { - @Test - fun `getAbsoluteFilePath() returns filePath prepended with dirPath`() { - val path = PathUtils.getAbsoluteFilePath("/anotherRoot/anotherDir", "root/dir/file") - assertThat(path, equalTo("/anotherRoot/anotherDir/root/dir/file")) - } - - @Test - fun `getAbsoluteFilePath() returns valid path when filePath does not start with seperator`() { - val path = PathUtils.getAbsoluteFilePath("/root/dir", "file") - assertThat(path, equalTo("/root/dir/file")) - } - - @Test - fun `getAbsoluteFilePath() returns filePath when it starts with dirPath`() { - val path = PathUtils.getAbsoluteFilePath("/root/dir", "/root/dir/file") - assertThat(path, equalTo("/root/dir/file")) - } - - @Test(expected = SecurityException::class) - fun `getAbsoluteFilePath() throws SecurityException when filePath is outside the dirPath`() { - PathUtils.getAbsoluteFilePath("/root/dir", "../tmp/file") - } - - @Test - fun `getAbsoluteFilePath() works when dirPath is not canonical`() { - val tempDir = TempFiles.createTempDir() - val nonCanonicalPath = - tempDir.canonicalPath + File.separator + ".." + File.separator + tempDir.name - assertThat(File(nonCanonicalPath).canonicalPath, equalTo(tempDir.canonicalPath)) - - val path = PathUtils.getAbsoluteFilePath(nonCanonicalPath, "file") - assertThat(path, equalTo(nonCanonicalPath + File.separator + "file")) - } +// @Test +// fun `getAbsoluteFilePath() returns filePath prepended with dirPath`() { +// val path = PathUtils.getAbsoluteFilePath("/anotherRoot/anotherDir", "root/dir/file") +// assertThat(path, equalTo("/anotherRoot/anotherDir/root/dir/file")) +// } +// +// @Test +// fun `getAbsoluteFilePath() returns valid path when filePath does not start with seperator`() { +// val path = PathUtils.getAbsoluteFilePath("/root/dir", "file") +// assertThat(path, equalTo("/root/dir/file")) +// } +// +// @Test +// fun `getAbsoluteFilePath() returns filePath when it starts with dirPath`() { +// val path = PathUtils.getAbsoluteFilePath("/root/dir", "/root/dir/file") +// assertThat(path, equalTo("/root/dir/file")) +// } +// +// @Test(expected = SecurityException::class) +// fun `getAbsoluteFilePath() throws SecurityException when filePath is outside the dirPath`() { +// PathUtils.getAbsoluteFilePath("/root/dir", "../tmp/file") +// } +// +// @Test +// fun `getAbsoluteFilePath() works when dirPath is not canonical`() { +// val tempDir = TempFiles.createTempDir() +// val nonCanonicalPath = +// tempDir.canonicalPath + File.separator + ".." + File.separator + tempDir.name +// assertThat(File(nonCanonicalPath).canonicalPath, equalTo(tempDir.canonicalPath)) +// +// val path = PathUtils.getAbsoluteFilePath(nonCanonicalPath, "file") +// assertThat(path, equalTo(nonCanonicalPath + File.separator + "file")) +// } @Test fun `getRelativeFilePath() returns filePath with dirPath removed`() { From f9377c42ca43ebd1f4ad243393aa2d448ce996de Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 21 Aug 2024 13:05:53 +0200 Subject: [PATCH 746/750] Log error instead of throwing exception --- .../collect/androidshared/utils/PathUtils.kt | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/utils/PathUtils.kt b/androidshared/src/main/java/org/odk/collect/androidshared/utils/PathUtils.kt index 1f20205bf03..a2ed621597a 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/utils/PathUtils.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/utils/PathUtils.kt @@ -1,17 +1,26 @@ package org.odk.collect.androidshared.utils +import timber.log.Timber import java.io.File object PathUtils { @JvmStatic fun getAbsoluteFilePath(dirPath: String, filePath: String): String { - val absolutePath = + val absoluteFilePath = if (filePath.startsWith(dirPath)) filePath else dirPath + File.separator + filePath - if (File(absolutePath).canonicalPath.startsWith(File(dirPath).canonicalPath)) { - return absolutePath - } else { - throw SecurityException("Contact support@getodk.org. Attempt to access file outside of Collect directory: $absolutePath") + val canonicalAbsoluteFilePath = File(absoluteFilePath).canonicalPath + val canonicalDirPath = File(dirPath).canonicalPath + if (!canonicalAbsoluteFilePath.startsWith(canonicalDirPath)) { + Timber.e( + "Attempt to access file outside of Collect directory:\n" + + "dirPath: $dirPath\n" + + "filePath: $filePath\n" + + "absoluteFilePath: $absoluteFilePath\n" + + "canonicalAbsoluteFilePath: $canonicalAbsoluteFilePath\n" + + "canonicalDirPath: $canonicalDirPath\n" + ) } + return absoluteFilePath } } From e3f3d60c49b0e81fe40f851baaeeba08c0d998e6 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 21 Aug 2024 16:19:09 +0200 Subject: [PATCH 747/750] Moved tests to the androidshared module --- .../collect/androidshared/utils/PathUtils.kt | 2 +- .../androidshared/utils/PathUtilsTest.kt | 38 +++++++++++++++++++ .../org/odk/collect/shared/PathUtilsTest.kt | 36 ------------------ 3 files changed, 39 insertions(+), 37 deletions(-) create mode 100644 androidshared/src/test/java/org/odk/collect/androidshared/utils/PathUtilsTest.kt diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/utils/PathUtils.kt b/androidshared/src/main/java/org/odk/collect/androidshared/utils/PathUtils.kt index a2ed621597a..2f2be8c598c 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/utils/PathUtils.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/utils/PathUtils.kt @@ -18,7 +18,7 @@ object PathUtils { "filePath: $filePath\n" + "absoluteFilePath: $absoluteFilePath\n" + "canonicalAbsoluteFilePath: $canonicalAbsoluteFilePath\n" + - "canonicalDirPath: $canonicalDirPath\n" + "canonicalDirPath: $canonicalDirPath" ) } return absoluteFilePath diff --git a/androidshared/src/test/java/org/odk/collect/androidshared/utils/PathUtilsTest.kt b/androidshared/src/test/java/org/odk/collect/androidshared/utils/PathUtilsTest.kt new file mode 100644 index 00000000000..472b285027d --- /dev/null +++ b/androidshared/src/test/java/org/odk/collect/androidshared/utils/PathUtilsTest.kt @@ -0,0 +1,38 @@ +package org.odk.collect.androidshared.utils + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.Test +import org.odk.collect.shared.TempFiles +import java.io.File + +class PathUtilsTest { + @Test + fun `getAbsoluteFilePath() returns filePath prepended with dirPath`() { + val path = PathUtils.getAbsoluteFilePath("/anotherRoot/anotherDir", "root/dir/file") + assertThat(path, equalTo("/anotherRoot/anotherDir/root/dir/file")) + } + + @Test + fun `getAbsoluteFilePath() returns valid path when filePath does not start with seperator`() { + val path = PathUtils.getAbsoluteFilePath("/root/dir", "file") + assertThat(path, equalTo("/root/dir/file")) + } + + @Test + fun `getAbsoluteFilePath() returns filePath when it starts with dirPath`() { + val path = PathUtils.getAbsoluteFilePath("/root/dir", "/root/dir/file") + assertThat(path, equalTo("/root/dir/file")) + } + + @Test + fun `getAbsoluteFilePath() works when dirPath is not canonical`() { + val tempDir = TempFiles.createTempDir() + val nonCanonicalPath = + tempDir.canonicalPath + File.separator + ".." + File.separator + tempDir.name + assertThat(File(nonCanonicalPath).canonicalPath, equalTo(tempDir.canonicalPath)) + + val path = PathUtils.getAbsoluteFilePath(nonCanonicalPath, "file") + assertThat(path, equalTo(nonCanonicalPath + File.separator + "file")) + } +} diff --git a/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt b/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt index b9f66174409..b7c2dede84b 100644 --- a/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt +++ b/shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt @@ -3,44 +3,8 @@ package org.odk.collect.shared import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo import org.junit.Test -// import java.io.File class PathUtilsTest { - -// @Test -// fun `getAbsoluteFilePath() returns filePath prepended with dirPath`() { -// val path = PathUtils.getAbsoluteFilePath("/anotherRoot/anotherDir", "root/dir/file") -// assertThat(path, equalTo("/anotherRoot/anotherDir/root/dir/file")) -// } -// -// @Test -// fun `getAbsoluteFilePath() returns valid path when filePath does not start with seperator`() { -// val path = PathUtils.getAbsoluteFilePath("/root/dir", "file") -// assertThat(path, equalTo("/root/dir/file")) -// } -// -// @Test -// fun `getAbsoluteFilePath() returns filePath when it starts with dirPath`() { -// val path = PathUtils.getAbsoluteFilePath("/root/dir", "/root/dir/file") -// assertThat(path, equalTo("/root/dir/file")) -// } -// -// @Test(expected = SecurityException::class) -// fun `getAbsoluteFilePath() throws SecurityException when filePath is outside the dirPath`() { -// PathUtils.getAbsoluteFilePath("/root/dir", "../tmp/file") -// } -// -// @Test -// fun `getAbsoluteFilePath() works when dirPath is not canonical`() { -// val tempDir = TempFiles.createTempDir() -// val nonCanonicalPath = -// tempDir.canonicalPath + File.separator + ".." + File.separator + tempDir.name -// assertThat(File(nonCanonicalPath).canonicalPath, equalTo(tempDir.canonicalPath)) -// -// val path = PathUtils.getAbsoluteFilePath(nonCanonicalPath, "file") -// assertThat(path, equalTo(nonCanonicalPath + File.separator + "file")) -// } - @Test fun `getRelativeFilePath() returns filePath with dirPath removed`() { val path = PathUtils.getRelativeFilePath("/root/dir", "/root/dir/file") From 98ec822838ca96826c687c7b2f1994928cb6851e Mon Sep 17 00:00:00 2001 From: Gareth Bestor Date: Mon, 21 Oct 2024 16:09:49 +1300 Subject: [PATCH 748/750] Update KoboCollect to ODK Collect v2024.2.4 --- collect_app/build.gradle | 2 +- collect_app/google-services.json | 34 +- collect_app/src/debug/google-services.json | 34 +- collect_app/src/main/AndroidManifest.xml | 3 +- .../src/main/ic_launcher-playstore.png | Bin 0 -> 13242 bytes .../res/drawable-anydpi-v24/ic_stat_name.xml | 18 + .../res/drawable-anydpi/ic_action_name.xml | 17 + .../main/res/drawable-hdpi/ic_action_name.png | Bin 0 -> 649 bytes .../main/res/drawable-hdpi/ic_stat_name.png | Bin 0 -> 559 bytes .../main/res/drawable-mdpi/ic_action_name.png | Bin 0 -> 420 bytes .../main/res/drawable-mdpi/ic_stat_name.png | Bin 0 -> 377 bytes .../res/drawable-xhdpi/ic_action_name.png | Bin 0 -> 863 bytes .../main/res/drawable-xhdpi/ic_stat_name.png | Bin 0 -> 739 bytes .../res/drawable-xxhdpi/ic_action_name.png | Bin 0 -> 1379 bytes .../main/res/drawable-xxhdpi/ic_stat_name.png | Bin 0 -> 1174 bytes .../res/drawable/ic_launcher_foreground.xml | 17 + .../res/drawable/ic_splash_screen_icon.xml | 14 +- .../res/drawable/kobologo_symbol_cropped.xml | 12 + .../main/res/layout/first_launch_layout.xml | 2 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1084 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2456 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 776 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1578 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1428 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3406 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2106 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5362 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 2766 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7514 bytes .../res/values/ic_launcher_background.xml | 4 + gradle.properties | 4 +- node_modules/.bin/replace | 1 + node_modules/.bin/search | 1 + node_modules/.package-lock.json | 365 ++++++ node_modules/ansi-regex/index.d.ts | 37 + node_modules/ansi-regex/index.js | 10 + node_modules/ansi-regex/license | 9 + node_modules/ansi-regex/package.json | 55 + node_modules/ansi-regex/readme.md | 78 ++ node_modules/ansi-styles/index.js | 165 +++ node_modules/ansi-styles/license | 9 + node_modules/ansi-styles/package.json | 56 + node_modules/ansi-styles/readme.md | 147 +++ .../balanced-match/.github/FUNDING.yml | 2 + node_modules/balanced-match/LICENSE.md | 21 + node_modules/balanced-match/README.md | 97 ++ node_modules/balanced-match/index.js | 62 + node_modules/balanced-match/package.json | 48 + node_modules/brace-expansion/LICENSE | 21 + node_modules/brace-expansion/README.md | 129 +++ node_modules/brace-expansion/index.js | 201 ++++ node_modules/brace-expansion/package.json | 47 + node_modules/camelcase/index.d.ts | 63 + node_modules/camelcase/index.js | 76 ++ node_modules/camelcase/license | 9 + node_modules/camelcase/package.json | 43 + node_modules/camelcase/readme.md | 99 ++ node_modules/chalk/index.js | 228 ++++ node_modules/chalk/index.js.flow | 93 ++ node_modules/chalk/license | 9 + node_modules/chalk/package.json | 71 ++ node_modules/chalk/readme.md | 314 +++++ node_modules/chalk/templates.js | 128 ++ node_modules/chalk/types/index.d.ts | 97 ++ node_modules/cliui/CHANGELOG.md | 76 ++ node_modules/cliui/LICENSE.txt | 14 + node_modules/cliui/README.md | 115 ++ node_modules/cliui/index.js | 354 ++++++ node_modules/cliui/package.json | 65 ++ node_modules/color-convert/CHANGELOG.md | 54 + node_modules/color-convert/LICENSE | 21 + node_modules/color-convert/README.md | 68 ++ node_modules/color-convert/conversions.js | 868 ++++++++++++++ node_modules/color-convert/index.js | 78 ++ node_modules/color-convert/package.json | 46 + node_modules/color-convert/route.js | 97 ++ node_modules/color-name/.eslintrc.json | 43 + node_modules/color-name/.npmignore | 107 ++ node_modules/color-name/LICENSE | 8 + node_modules/color-name/README.md | 11 + node_modules/color-name/index.js | 152 +++ node_modules/color-name/package.json | 25 + node_modules/color-name/test.js | 7 + node_modules/concat-map/.travis.yml | 4 + node_modules/concat-map/LICENSE | 18 + node_modules/concat-map/README.markdown | 62 + node_modules/concat-map/example/map.js | 6 + node_modules/concat-map/index.js | 13 + node_modules/concat-map/package.json | 43 + node_modules/concat-map/test/map.js | 39 + node_modules/decamelize/index.js | 13 + node_modules/decamelize/license | 21 + node_modules/decamelize/package.json | 38 + node_modules/decamelize/readme.md | 48 + node_modules/emoji-regex/LICENSE-MIT.txt | 20 + node_modules/emoji-regex/README.md | 73 ++ node_modules/emoji-regex/es2015/index.js | 6 + node_modules/emoji-regex/es2015/text.js | 6 + node_modules/emoji-regex/index.d.ts | 23 + node_modules/emoji-regex/index.js | 6 + node_modules/emoji-regex/package.json | 50 + node_modules/emoji-regex/text.js | 6 + node_modules/escape-string-regexp/index.js | 11 + node_modules/escape-string-regexp/license | 21 + .../escape-string-regexp/package.json | 41 + node_modules/escape-string-regexp/readme.md | 27 + node_modules/find-up/index.d.ts | 137 +++ node_modules/find-up/index.js | 89 ++ node_modules/find-up/license | 9 + node_modules/find-up/package.json | 53 + node_modules/find-up/readme.md | 156 +++ node_modules/get-caller-file/LICENSE.md | 6 + node_modules/get-caller-file/README.md | 41 + node_modules/get-caller-file/index.d.ts | 2 + node_modules/get-caller-file/index.js | 22 + node_modules/get-caller-file/index.js.map | 1 + node_modules/get-caller-file/package.json | 42 + node_modules/has-flag/index.js | 8 + node_modules/has-flag/license | 9 + node_modules/has-flag/package.json | 44 + node_modules/has-flag/readme.md | 70 ++ .../is-fullwidth-code-point/index.d.ts | 17 + node_modules/is-fullwidth-code-point/index.js | 50 + node_modules/is-fullwidth-code-point/license | 9 + .../is-fullwidth-code-point/package.json | 42 + .../is-fullwidth-code-point/readme.md | 39 + node_modules/locate-path/index.d.ts | 83 ++ node_modules/locate-path/index.js | 65 ++ node_modules/locate-path/license | 9 + node_modules/locate-path/package.json | 45 + node_modules/locate-path/readme.md | 122 ++ node_modules/minimatch/LICENSE | 15 + node_modules/minimatch/README.md | 209 ++++ node_modules/minimatch/minimatch.js | 952 +++++++++++++++ node_modules/minimatch/package.json | 30 + node_modules/p-limit/index.d.ts | 38 + node_modules/p-limit/index.js | 57 + node_modules/p-limit/license | 9 + node_modules/p-limit/package.json | 52 + node_modules/p-limit/readme.md | 101 ++ node_modules/p-locate/index.d.ts | 64 + node_modules/p-locate/index.js | 52 + node_modules/p-locate/license | 9 + node_modules/p-locate/package.json | 53 + node_modules/p-locate/readme.md | 90 ++ node_modules/p-try/index.d.ts | 39 + node_modules/p-try/index.js | 9 + node_modules/p-try/license | 9 + node_modules/p-try/package.json | 42 + node_modules/p-try/readme.md | 58 + node_modules/path-exists/index.d.ts | 28 + node_modules/path-exists/index.js | 23 + node_modules/path-exists/license | 9 + node_modules/path-exists/package.json | 39 + node_modules/path-exists/readme.md | 52 + node_modules/replace/.github/workflows/ci.yml | 20 + node_modules/replace/LICENSE | 20 + node_modules/replace/README.md | 115 ++ node_modules/replace/defaultignore | 64 + node_modules/replace/out.txt | 1 + node_modules/replace/package.json | 36 + node_modules/replace/replace.js | 209 ++++ node_modules/replace/test/README.md | 15 + node_modules/replace/test/cli.js | 29 + node_modules/replace/test/index.js | 3 + node_modules/replace/test/paths.js | 129 +++ node_modules/replace/test/sanity.js | 139 +++ .../replace/test/test_files/test_basic.txt | 1 + .../replace/test/test_files/test_case.txt | 1 + .../test/test_files/test_multiline.txt | 2 + .../replace/test/test_files/test_numbers.txt | 1 + .../test/test_files/test_paths/sample1.txt | 1 + .../test/test_files/test_paths/test.png | 1 + .../test/test_files/test_paths/test1.txt | 1 + .../test/test_files/test_paths/test2.txt | 1 + .../replace/test/test_files/test_preview.txt | 1 + node_modules/replace/testfunc.js | 3 + node_modules/require-directory/.jshintrc | 67 ++ node_modules/require-directory/.npmignore | 1 + node_modules/require-directory/.travis.yml | 3 + node_modules/require-directory/LICENSE | 22 + .../require-directory/README.markdown | 184 +++ node_modules/require-directory/index.js | 86 ++ node_modules/require-directory/package.json | 40 + .../require-main-filename/CHANGELOG.md | 26 + .../require-main-filename/LICENSE.txt | 14 + node_modules/require-main-filename/README.md | 26 + node_modules/require-main-filename/index.js | 18 + .../require-main-filename/package.json | 35 + node_modules/set-blocking/CHANGELOG.md | 26 + node_modules/set-blocking/LICENSE.txt | 14 + node_modules/set-blocking/README.md | 31 + node_modules/set-blocking/index.js | 7 + node_modules/set-blocking/package.json | 42 + node_modules/string-width/index.d.ts | 29 + node_modules/string-width/index.js | 47 + node_modules/string-width/license | 9 + node_modules/string-width/package.json | 56 + node_modules/string-width/readme.md | 50 + node_modules/strip-ansi/index.d.ts | 17 + node_modules/strip-ansi/index.js | 4 + node_modules/strip-ansi/license | 9 + node_modules/strip-ansi/package.json | 54 + node_modules/strip-ansi/readme.md | 46 + node_modules/supports-color/browser.js | 5 + node_modules/supports-color/index.js | 131 +++ node_modules/supports-color/license | 9 + node_modules/supports-color/package.json | 53 + node_modules/supports-color/readme.md | 66 ++ node_modules/which-module/LICENSE | 13 + node_modules/which-module/README.md | 58 + node_modules/which-module/index.js | 9 + node_modules/which-module/package.json | 41 + node_modules/wrap-ansi/index.js | 186 +++ node_modules/wrap-ansi/license | 9 + .../node_modules/ansi-styles/index.d.ts | 345 ++++++ .../node_modules/ansi-styles/index.js | 163 +++ .../node_modules/ansi-styles/license | 9 + .../node_modules/ansi-styles/package.json | 56 + .../node_modules/ansi-styles/readme.md | 152 +++ .../node_modules/color-convert/CHANGELOG.md | 54 + .../node_modules/color-convert/LICENSE | 21 + .../node_modules/color-convert/README.md | 68 ++ .../node_modules/color-convert/conversions.js | 839 ++++++++++++++ .../node_modules/color-convert/index.js | 81 ++ .../node_modules/color-convert/package.json | 48 + .../node_modules/color-convert/route.js | 97 ++ .../wrap-ansi/node_modules/color-name/LICENSE | 8 + .../node_modules/color-name/README.md | 11 + .../node_modules/color-name/index.js | 152 +++ .../node_modules/color-name/package.json | 28 + node_modules/wrap-ansi/package.json | 61 + node_modules/wrap-ansi/readme.md | 97 ++ node_modules/y18n/CHANGELOG.md | 35 + node_modules/y18n/LICENSE | 13 + node_modules/y18n/README.md | 109 ++ node_modules/y18n/index.js | 188 +++ node_modules/y18n/package.json | 39 + node_modules/yargs-parser/CHANGELOG.md | 601 ++++++++++ node_modules/yargs-parser/LICENSE.txt | 14 + node_modules/yargs-parser/README.md | 449 +++++++ node_modules/yargs-parser/index.js | 1032 +++++++++++++++++ .../yargs-parser/lib/tokenize-arg-string.js | 40 + node_modules/yargs-parser/package.json | 46 + node_modules/yargs/CHANGELOG.md | 420 +++++++ node_modules/yargs/LICENSE | 21 + node_modules/yargs/README.md | 140 +++ node_modules/yargs/index.js | 40 + node_modules/yargs/locales/be.json | 46 + node_modules/yargs/locales/de.json | 46 + node_modules/yargs/locales/en.json | 51 + node_modules/yargs/locales/es.json | 46 + node_modules/yargs/locales/fi.json | 49 + node_modules/yargs/locales/fr.json | 53 + node_modules/yargs/locales/hi.json | 49 + node_modules/yargs/locales/hu.json | 46 + node_modules/yargs/locales/id.json | 50 + node_modules/yargs/locales/it.json | 46 + node_modules/yargs/locales/ja.json | 51 + node_modules/yargs/locales/ko.json | 49 + node_modules/yargs/locales/nb.json | 44 + node_modules/yargs/locales/nl.json | 49 + node_modules/yargs/locales/nn.json | 44 + node_modules/yargs/locales/pirate.json | 13 + node_modules/yargs/locales/pl.json | 49 + node_modules/yargs/locales/pt.json | 45 + node_modules/yargs/locales/pt_BR.json | 48 + node_modules/yargs/locales/ru.json | 46 + node_modules/yargs/locales/th.json | 46 + node_modules/yargs/locales/tr.json | 48 + node_modules/yargs/locales/zh_CN.json | 48 + node_modules/yargs/locales/zh_TW.json | 47 + node_modules/yargs/package.json | 92 ++ node_modules/yargs/yargs.js | 14 + package-lock.json | 370 ++++++ package.json | 5 + strings/src/main/res/values-pt/strings.xml | 40 +- strings/src/main/res/values-sl/strings.xml | 30 +- .../src/main/res/values-zh-rTW/strings.xml | 24 +- strings/src/main/res/values-zh/strings.xml | 2 +- 282 files changed, 18394 insertions(+), 80 deletions(-) create mode 100644 collect_app/src/main/ic_launcher-playstore.png create mode 100644 collect_app/src/main/res/drawable-anydpi-v24/ic_stat_name.xml create mode 100644 collect_app/src/main/res/drawable-anydpi/ic_action_name.xml create mode 100644 collect_app/src/main/res/drawable-hdpi/ic_action_name.png create mode 100644 collect_app/src/main/res/drawable-hdpi/ic_stat_name.png create mode 100644 collect_app/src/main/res/drawable-mdpi/ic_action_name.png create mode 100644 collect_app/src/main/res/drawable-mdpi/ic_stat_name.png create mode 100644 collect_app/src/main/res/drawable-xhdpi/ic_action_name.png create mode 100644 collect_app/src/main/res/drawable-xhdpi/ic_stat_name.png create mode 100644 collect_app/src/main/res/drawable-xxhdpi/ic_action_name.png create mode 100644 collect_app/src/main/res/drawable-xxhdpi/ic_stat_name.png create mode 100644 collect_app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 collect_app/src/main/res/drawable/kobologo_symbol_cropped.xml create mode 100644 collect_app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 collect_app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 collect_app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 collect_app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 collect_app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 collect_app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 collect_app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 collect_app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 collect_app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 collect_app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 collect_app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 collect_app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 collect_app/src/main/res/values/ic_launcher_background.xml create mode 120000 node_modules/.bin/replace create mode 120000 node_modules/.bin/search create mode 100644 node_modules/.package-lock.json create mode 100644 node_modules/ansi-regex/index.d.ts create mode 100644 node_modules/ansi-regex/index.js create mode 100644 node_modules/ansi-regex/license create mode 100644 node_modules/ansi-regex/package.json create mode 100644 node_modules/ansi-regex/readme.md create mode 100644 node_modules/ansi-styles/index.js create mode 100644 node_modules/ansi-styles/license create mode 100644 node_modules/ansi-styles/package.json create mode 100644 node_modules/ansi-styles/readme.md create mode 100644 node_modules/balanced-match/.github/FUNDING.yml create mode 100644 node_modules/balanced-match/LICENSE.md create mode 100644 node_modules/balanced-match/README.md create mode 100644 node_modules/balanced-match/index.js create mode 100644 node_modules/balanced-match/package.json create mode 100644 node_modules/brace-expansion/LICENSE create mode 100644 node_modules/brace-expansion/README.md create mode 100644 node_modules/brace-expansion/index.js create mode 100644 node_modules/brace-expansion/package.json create mode 100644 node_modules/camelcase/index.d.ts create mode 100644 node_modules/camelcase/index.js create mode 100644 node_modules/camelcase/license create mode 100644 node_modules/camelcase/package.json create mode 100644 node_modules/camelcase/readme.md create mode 100644 node_modules/chalk/index.js create mode 100644 node_modules/chalk/index.js.flow create mode 100644 node_modules/chalk/license create mode 100644 node_modules/chalk/package.json create mode 100644 node_modules/chalk/readme.md create mode 100644 node_modules/chalk/templates.js create mode 100644 node_modules/chalk/types/index.d.ts create mode 100644 node_modules/cliui/CHANGELOG.md create mode 100644 node_modules/cliui/LICENSE.txt create mode 100644 node_modules/cliui/README.md create mode 100644 node_modules/cliui/index.js create mode 100644 node_modules/cliui/package.json create mode 100644 node_modules/color-convert/CHANGELOG.md create mode 100644 node_modules/color-convert/LICENSE create mode 100644 node_modules/color-convert/README.md create mode 100644 node_modules/color-convert/conversions.js create mode 100644 node_modules/color-convert/index.js create mode 100644 node_modules/color-convert/package.json create mode 100644 node_modules/color-convert/route.js create mode 100644 node_modules/color-name/.eslintrc.json create mode 100644 node_modules/color-name/.npmignore create mode 100644 node_modules/color-name/LICENSE create mode 100644 node_modules/color-name/README.md create mode 100644 node_modules/color-name/index.js create mode 100644 node_modules/color-name/package.json create mode 100644 node_modules/color-name/test.js create mode 100644 node_modules/concat-map/.travis.yml create mode 100644 node_modules/concat-map/LICENSE create mode 100644 node_modules/concat-map/README.markdown create mode 100644 node_modules/concat-map/example/map.js create mode 100644 node_modules/concat-map/index.js create mode 100644 node_modules/concat-map/package.json create mode 100644 node_modules/concat-map/test/map.js create mode 100644 node_modules/decamelize/index.js create mode 100644 node_modules/decamelize/license create mode 100644 node_modules/decamelize/package.json create mode 100644 node_modules/decamelize/readme.md create mode 100644 node_modules/emoji-regex/LICENSE-MIT.txt create mode 100644 node_modules/emoji-regex/README.md create mode 100644 node_modules/emoji-regex/es2015/index.js create mode 100644 node_modules/emoji-regex/es2015/text.js create mode 100644 node_modules/emoji-regex/index.d.ts create mode 100644 node_modules/emoji-regex/index.js create mode 100644 node_modules/emoji-regex/package.json create mode 100644 node_modules/emoji-regex/text.js create mode 100644 node_modules/escape-string-regexp/index.js create mode 100644 node_modules/escape-string-regexp/license create mode 100644 node_modules/escape-string-regexp/package.json create mode 100644 node_modules/escape-string-regexp/readme.md create mode 100644 node_modules/find-up/index.d.ts create mode 100644 node_modules/find-up/index.js create mode 100644 node_modules/find-up/license create mode 100644 node_modules/find-up/package.json create mode 100644 node_modules/find-up/readme.md create mode 100644 node_modules/get-caller-file/LICENSE.md create mode 100644 node_modules/get-caller-file/README.md create mode 100644 node_modules/get-caller-file/index.d.ts create mode 100644 node_modules/get-caller-file/index.js create mode 100644 node_modules/get-caller-file/index.js.map create mode 100644 node_modules/get-caller-file/package.json create mode 100644 node_modules/has-flag/index.js create mode 100644 node_modules/has-flag/license create mode 100644 node_modules/has-flag/package.json create mode 100644 node_modules/has-flag/readme.md create mode 100644 node_modules/is-fullwidth-code-point/index.d.ts create mode 100644 node_modules/is-fullwidth-code-point/index.js create mode 100644 node_modules/is-fullwidth-code-point/license create mode 100644 node_modules/is-fullwidth-code-point/package.json create mode 100644 node_modules/is-fullwidth-code-point/readme.md create mode 100644 node_modules/locate-path/index.d.ts create mode 100644 node_modules/locate-path/index.js create mode 100644 node_modules/locate-path/license create mode 100644 node_modules/locate-path/package.json create mode 100644 node_modules/locate-path/readme.md create mode 100644 node_modules/minimatch/LICENSE create mode 100644 node_modules/minimatch/README.md create mode 100644 node_modules/minimatch/minimatch.js create mode 100644 node_modules/minimatch/package.json create mode 100644 node_modules/p-limit/index.d.ts create mode 100644 node_modules/p-limit/index.js create mode 100644 node_modules/p-limit/license create mode 100644 node_modules/p-limit/package.json create mode 100644 node_modules/p-limit/readme.md create mode 100644 node_modules/p-locate/index.d.ts create mode 100644 node_modules/p-locate/index.js create mode 100644 node_modules/p-locate/license create mode 100644 node_modules/p-locate/package.json create mode 100644 node_modules/p-locate/readme.md create mode 100644 node_modules/p-try/index.d.ts create mode 100644 node_modules/p-try/index.js create mode 100644 node_modules/p-try/license create mode 100644 node_modules/p-try/package.json create mode 100644 node_modules/p-try/readme.md create mode 100644 node_modules/path-exists/index.d.ts create mode 100644 node_modules/path-exists/index.js create mode 100644 node_modules/path-exists/license create mode 100644 node_modules/path-exists/package.json create mode 100644 node_modules/path-exists/readme.md create mode 100644 node_modules/replace/.github/workflows/ci.yml create mode 100644 node_modules/replace/LICENSE create mode 100644 node_modules/replace/README.md create mode 100644 node_modules/replace/defaultignore create mode 100644 node_modules/replace/out.txt create mode 100644 node_modules/replace/package.json create mode 100644 node_modules/replace/replace.js create mode 100644 node_modules/replace/test/README.md create mode 100644 node_modules/replace/test/cli.js create mode 100644 node_modules/replace/test/index.js create mode 100644 node_modules/replace/test/paths.js create mode 100644 node_modules/replace/test/sanity.js create mode 100644 node_modules/replace/test/test_files/test_basic.txt create mode 100644 node_modules/replace/test/test_files/test_case.txt create mode 100644 node_modules/replace/test/test_files/test_multiline.txt create mode 100644 node_modules/replace/test/test_files/test_numbers.txt create mode 100644 node_modules/replace/test/test_files/test_paths/sample1.txt create mode 100644 node_modules/replace/test/test_files/test_paths/test.png create mode 100644 node_modules/replace/test/test_files/test_paths/test1.txt create mode 100644 node_modules/replace/test/test_files/test_paths/test2.txt create mode 100644 node_modules/replace/test/test_files/test_preview.txt create mode 100644 node_modules/replace/testfunc.js create mode 100644 node_modules/require-directory/.jshintrc create mode 100644 node_modules/require-directory/.npmignore create mode 100644 node_modules/require-directory/.travis.yml create mode 100644 node_modules/require-directory/LICENSE create mode 100644 node_modules/require-directory/README.markdown create mode 100644 node_modules/require-directory/index.js create mode 100644 node_modules/require-directory/package.json create mode 100644 node_modules/require-main-filename/CHANGELOG.md create mode 100644 node_modules/require-main-filename/LICENSE.txt create mode 100644 node_modules/require-main-filename/README.md create mode 100644 node_modules/require-main-filename/index.js create mode 100644 node_modules/require-main-filename/package.json create mode 100644 node_modules/set-blocking/CHANGELOG.md create mode 100644 node_modules/set-blocking/LICENSE.txt create mode 100644 node_modules/set-blocking/README.md create mode 100644 node_modules/set-blocking/index.js create mode 100644 node_modules/set-blocking/package.json create mode 100644 node_modules/string-width/index.d.ts create mode 100644 node_modules/string-width/index.js create mode 100644 node_modules/string-width/license create mode 100644 node_modules/string-width/package.json create mode 100644 node_modules/string-width/readme.md create mode 100644 node_modules/strip-ansi/index.d.ts create mode 100644 node_modules/strip-ansi/index.js create mode 100644 node_modules/strip-ansi/license create mode 100644 node_modules/strip-ansi/package.json create mode 100644 node_modules/strip-ansi/readme.md create mode 100644 node_modules/supports-color/browser.js create mode 100644 node_modules/supports-color/index.js create mode 100644 node_modules/supports-color/license create mode 100644 node_modules/supports-color/package.json create mode 100644 node_modules/supports-color/readme.md create mode 100644 node_modules/which-module/LICENSE create mode 100644 node_modules/which-module/README.md create mode 100644 node_modules/which-module/index.js create mode 100644 node_modules/which-module/package.json create mode 100755 node_modules/wrap-ansi/index.js create mode 100644 node_modules/wrap-ansi/license create mode 100644 node_modules/wrap-ansi/node_modules/ansi-styles/index.d.ts create mode 100644 node_modules/wrap-ansi/node_modules/ansi-styles/index.js create mode 100644 node_modules/wrap-ansi/node_modules/ansi-styles/license create mode 100644 node_modules/wrap-ansi/node_modules/ansi-styles/package.json create mode 100644 node_modules/wrap-ansi/node_modules/ansi-styles/readme.md create mode 100644 node_modules/wrap-ansi/node_modules/color-convert/CHANGELOG.md create mode 100644 node_modules/wrap-ansi/node_modules/color-convert/LICENSE create mode 100644 node_modules/wrap-ansi/node_modules/color-convert/README.md create mode 100644 node_modules/wrap-ansi/node_modules/color-convert/conversions.js create mode 100644 node_modules/wrap-ansi/node_modules/color-convert/index.js create mode 100644 node_modules/wrap-ansi/node_modules/color-convert/package.json create mode 100644 node_modules/wrap-ansi/node_modules/color-convert/route.js create mode 100644 node_modules/wrap-ansi/node_modules/color-name/LICENSE create mode 100644 node_modules/wrap-ansi/node_modules/color-name/README.md create mode 100644 node_modules/wrap-ansi/node_modules/color-name/index.js create mode 100644 node_modules/wrap-ansi/node_modules/color-name/package.json create mode 100644 node_modules/wrap-ansi/package.json create mode 100644 node_modules/wrap-ansi/readme.md create mode 100644 node_modules/y18n/CHANGELOG.md create mode 100644 node_modules/y18n/LICENSE create mode 100644 node_modules/y18n/README.md create mode 100644 node_modules/y18n/index.js create mode 100644 node_modules/y18n/package.json create mode 100644 node_modules/yargs-parser/CHANGELOG.md create mode 100644 node_modules/yargs-parser/LICENSE.txt create mode 100644 node_modules/yargs-parser/README.md create mode 100644 node_modules/yargs-parser/index.js create mode 100644 node_modules/yargs-parser/lib/tokenize-arg-string.js create mode 100644 node_modules/yargs-parser/package.json create mode 100644 node_modules/yargs/CHANGELOG.md create mode 100644 node_modules/yargs/LICENSE create mode 100644 node_modules/yargs/README.md create mode 100644 node_modules/yargs/index.js create mode 100644 node_modules/yargs/locales/be.json create mode 100644 node_modules/yargs/locales/de.json create mode 100644 node_modules/yargs/locales/en.json create mode 100644 node_modules/yargs/locales/es.json create mode 100644 node_modules/yargs/locales/fi.json create mode 100644 node_modules/yargs/locales/fr.json create mode 100644 node_modules/yargs/locales/hi.json create mode 100644 node_modules/yargs/locales/hu.json create mode 100644 node_modules/yargs/locales/id.json create mode 100644 node_modules/yargs/locales/it.json create mode 100644 node_modules/yargs/locales/ja.json create mode 100644 node_modules/yargs/locales/ko.json create mode 100644 node_modules/yargs/locales/nb.json create mode 100644 node_modules/yargs/locales/nl.json create mode 100644 node_modules/yargs/locales/nn.json create mode 100644 node_modules/yargs/locales/pirate.json create mode 100644 node_modules/yargs/locales/pl.json create mode 100644 node_modules/yargs/locales/pt.json create mode 100644 node_modules/yargs/locales/pt_BR.json create mode 100644 node_modules/yargs/locales/ru.json create mode 100644 node_modules/yargs/locales/th.json create mode 100644 node_modules/yargs/locales/tr.json create mode 100644 node_modules/yargs/locales/zh_CN.json create mode 100644 node_modules/yargs/locales/zh_TW.json create mode 100644 node_modules/yargs/package.json create mode 100644 node_modules/yargs/yargs.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/collect_app/build.gradle b/collect_app/build.gradle index 8f0b3e5ea17..603d08003dd 100644 --- a/collect_app/build.gradle +++ b/collect_app/build.gradle @@ -77,7 +77,7 @@ android { applicationId('org.koboc.collect.android') minSdkVersion Versions.android_min_sdk targetSdkVersion Versions.android_target_sdk - versionCode 4147 + versionCode 4148 versionName getVersionName() testInstrumentationRunner('androidx.test.runner.AndroidJUnitRunner') multiDexEnabled true diff --git a/collect_app/google-services.json b/collect_app/google-services.json index b5fc294e230..1e6a5a9b311 100644 --- a/collect_app/google-services.json +++ b/collect_app/google-services.json @@ -1,34 +1,48 @@ { "project_info": { - "project_number": "640412404956", - "firebase_url": "https://opendatakit-forks.firebaseio.com", - "project_id": "opendatakit-forks", - "storage_bucket": "opendatakit-forks.appspot.com" + "project_number": "659901748622", + "firebase_url": "https://kobocollect-6cf1e.firebaseio.com", + "project_id": "kobocollect-6cf1e", + "storage_bucket": "kobocollect-6cf1e.appspot.com" }, "client": [ { "client_info": { - "mobilesdk_app_id": "1:640412404956:android:730483f389563845cb7819", + "mobilesdk_app_id": "1:659901748622:android:3b0a466019a89dac", "android_client_info": { - "package_name": "org.odk.collect.android" + "package_name": "org.koboc.collect.android" } }, "oauth_client": [ { - "client_id": "640412404956-ag8do5nic02k70qr2k1a0mv3du8222hv.apps.googleusercontent.com", + "client_id": "659901748622-iekvk6f4ct9g4iomgr3j5vvja9gp71l0.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "org.koboc.collect.android", + "certificate_hash": "df1bd727d0cc9ca347720a38f2249552dc1865ec" + } + }, + { + "client_id": "659901748622-n371b3mfc92hc42vka989l8ae8u6v1c8.apps.googleusercontent.com", "client_type": 3 } ], "api_key": [ { - "current_key": "AIzaSyA5tuKwVrlKEs-7iMSpdTmAy6qSH-w0lik" + "current_key": "AIzaSyCkT_yrsm0Keynz2o_sOAs5N_ahmFmtJsU" } ], "services": { + "analytics_service": { + "status": 2, + "analytics_property": { + "tracking_id": "UA-111083836-1" + } + }, "appinvite_service": { "other_platform_oauth_client": [ { - "client_id": "640412404956-ag8do5nic02k70qr2k1a0mv3du8222hv.apps.googleusercontent.com", + "client_id": "659901748622-n371b3mfc92hc42vka989l8ae8u6v1c8.apps.googleusercontent.com", "client_type": 3 } ] @@ -37,4 +51,4 @@ } ], "configuration_version": "1" -} \ No newline at end of file +} diff --git a/collect_app/src/debug/google-services.json b/collect_app/src/debug/google-services.json index dd5b72899b3..1e6a5a9b311 100644 --- a/collect_app/src/debug/google-services.json +++ b/collect_app/src/debug/google-services.json @@ -1,34 +1,48 @@ { "project_info": { - "project_number": "462257085307", - "firebase_url": "https://opendatakit-debug.firebaseio.com", - "project_id": "opendatakit-debug", - "storage_bucket": "opendatakit-debug.appspot.com" + "project_number": "659901748622", + "firebase_url": "https://kobocollect-6cf1e.firebaseio.com", + "project_id": "kobocollect-6cf1e", + "storage_bucket": "kobocollect-6cf1e.appspot.com" }, "client": [ { "client_info": { - "mobilesdk_app_id": "1:462257085307:android:64acb5e3c9e68ab5173bde", + "mobilesdk_app_id": "1:659901748622:android:3b0a466019a89dac", "android_client_info": { - "package_name": "org.odk.collect.android" + "package_name": "org.koboc.collect.android" } }, "oauth_client": [ { - "client_id": "462257085307-o9ll0ogq8jm4ta3q9av2mf2rh4ume9vj.apps.googleusercontent.com", + "client_id": "659901748622-iekvk6f4ct9g4iomgr3j5vvja9gp71l0.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "org.koboc.collect.android", + "certificate_hash": "df1bd727d0cc9ca347720a38f2249552dc1865ec" + } + }, + { + "client_id": "659901748622-n371b3mfc92hc42vka989l8ae8u6v1c8.apps.googleusercontent.com", "client_type": 3 } ], "api_key": [ { - "current_key": "AIzaSyAClt29XaVTTO1m2R9u3WW-RimapSqjALg" + "current_key": "AIzaSyCkT_yrsm0Keynz2o_sOAs5N_ahmFmtJsU" } ], "services": { + "analytics_service": { + "status": 2, + "analytics_property": { + "tracking_id": "UA-111083836-1" + } + }, "appinvite_service": { "other_platform_oauth_client": [ { - "client_id": "462257085307-o9ll0ogq8jm4ta3q9av2mf2rh4ume9vj.apps.googleusercontent.com", + "client_id": "659901748622-n371b3mfc92hc42vka989l8ae8u6v1c8.apps.googleusercontent.com", "client_type": 3 } ] @@ -37,4 +51,4 @@ } ], "configuration_version": "1" -} \ No newline at end of file +} diff --git a/collect_app/src/main/AndroidManifest.xml b/collect_app/src/main/AndroidManifest.xml index 06e5d009872..00380bf6748 100644 --- a/collect_app/src/main/AndroidManifest.xml +++ b/collect_app/src/main/AndroidManifest.xml @@ -77,7 +77,8 @@ the specific language governing permissions and limitations under the License. OXbGz9jjV%HaoGWcCS84jk(pcP5T#qjIo|@ zUH($Xd`-jjv0>Zw>Ewxm6`}B+C5^e(0ve6NfnE6*a~<7rIVbWx^`QQEztsn=-obV~9PN2LpCls@di zIS3+G+*N5?i|{tTS0uRj=-0cHu2ou{>@3N^c(fI^e5KZ2Ej7(?@_mAuf>vlD z#PygXf`1{cg7`CYXnM$tYG;$}CdXI3g1vKNU>KgQNjCv2OQ*>%9BQHEP0gF~rNkSq zq1>mWFymuR<>=?ess(J!VWDhLSaP0oIGz~1_+CFv1-4kcGVN|;Y3W?dOi^}^*u|st z>~pIdVJqg`^_FI?Q$*Q(EwbOmJ3d2Ag%T&P#Z%*GSRq#V#bp*NWKl*-Q)lM3it|*p zCU!{tI{_6{Ky=Gl>0@#pz9tyMg3y^<^N|XQO@H7q=nS)vcRoiLrBi&w06pzo(?R)c zKe{Fx(oCHdN@9|9#l#S`a$LGwZP~ZF-2^;6N{V*hpi5!q>n>kk(zE`32zr~*P!9`o zVQ)HhwrpjB!+TS-59=OLQ#{<$(N=bo+q( zTCv?0_N?0b)A);rI%N(x29*pL#&F+jOeI}vdz8liMGFeG1Y;?Nk=^#1;&y+yC3Z<~CnRjBI=iW0Hl@Q;k zrnnQS_)lkO3}|VfkDY3F(zY?YACNF!c+b1-pTX$M=NB|x1Q3cj3=l(9S!G`*CX_cW z#6b|flk=7V)Q%kS4K(>)*t54_a5JVRNg{7A) zmWk!2U_x`{vV^tksygg;w4n3np>qs@=`EVVkwX$?W$i(AY}q+RaZ7y5^%r=VK72c_ zc$ERVSetMJD<}TkJ?K3$p1#YhGi-Z8BFEQ4@NoJr_u8fyVa&TxtOpNV3kI}PZ zSrQc^sq{b63d;J64Ab4fevXtWY9W`J$IlSBVN9p{dp!Qz&98?okEWZ0kCD`Vx2c%F zwbnCpjRgizY&9zhxK=aJTx2-}X^gM=2nIRkw!B}7gsB>4cq7NAe{m-a-eOR^&I)zh z7I8n0D*o6!%>HIU-4MUp6IZ}`C-@O-Sg!z-ZeK#`yBtF7Y)gZogkE*BF3(EPeVd)U zd86bM4Ro0_zh>mY*N%pBSk21CBfg3CKQL0v181J_^epQ?@wW_0sJD~-u+IhS*8573 z=|dK+n3F?+@?2~?UlEZbjIo|V<6tb-S zs3_E`0x9ahDX};6`PRhVb-g+M?FcTkuJLAj>8>a9)nEpQ(^qB?pQk=q8Q@JhIh8l+ zS%{2vfB5|Z9mFWi+bP#KU8JnH63MHoN5q;na&wi3(?04r13u<0xt4NK^GTSTyFSFv zD!o3}D;uR~I32*Jq&gKl-|!X(nQ5Nu&)zCjc;6oT-BQ};Sby(4WPNAm1(iIlnwr`a zgnGuUMRrb#eQr#`n9-?bYR^#akE}FI6Y7UY|bdj7ziC`Lg#N_a-MXXCFLj2wotQal+__c<&-6C>vp^J z-IJTtY>nS2h2FE2%&_GOHA-(ui?>NuT7oTwl%Zs-V3X0u0UNi#JJ6~ecNk8Z1 z#kGye8P6%KHsJ`{|8H*=QqzvPMW(8rf$bddZI|GUj}#@CX&~k7#qR^`I%iWqb-`7c z9LXN&7_|HaNP-@G@}12kys^;Q&CD4W;XQl@`PrVwokm#|s(^7{w+?MI;}|3G0ky8NgtTF>T(sUeGMmS)C-ucva^Zn2`2%=Zz8NDCmw(cP{deyLS)s2=(E*=+QE?UaWFl9~QrE9ae z-|zBzu<;9<2GJ8?XNhS=ol&#_3f25c+30MV>F7m6tb(MOY^X3MD8)Pzd%3nwBH72% zd*v(ENUf{~m1Zau!jyjAyy4naPO$5Z!tPwzI~ktqU|-f!vb9|^WdJE0@^ktD{EUoD zrhw$2j_lSHxBjeNuA_rszc8yca8R5dqK@lWnwkvT^iEH6nS>18LMVOZ6AKYj7@W3i zq)Bp5M^jVtbY+erch7Inflsgm!+?=(m^MEXWkN?3+IXl%iy{SpUB*; z3h3~ZHkt_S?SoJw4hi^wXwF0FADVXZ<>da_wb4(Zlk$~DPzt@A9Rq62iW-tuHf}y$ zYot$#bc0lZtG>hWgt|6OZM!WTa%P0~y*A@e$COYZqW<)0PLt6utk5%H(ySI|H=aw{ zu*A=ysmkaR{Q*)apQkCQ=UKO#{_%@KO!DQ^*F&t3E&*gsbtUH(GH%OXE(p28bYX5* z=%weSGBN&T*5zKomZjK>FQ71`l8In{jgW`z8pE%p76L=%h~(clq|#oESsk9aI9yq` z8%sk3UQ}t7j3P{9N5#*__|)({4%a!>%`7pwv-_N+Erp@E2=rcQH|DRZ$4VJW-v97-V$Q882MT!$2?4A8O|)sj2b(@rS|q=I7Q z=#31{WQ=3txrfA%#e=(+JI6_USVnP3P7!$JdDeif;9 zV+yH@`8#N~j+&O*x(uiH1nF5ceZ@|ehhsj;H;lpxjg?YsTFN?FrN!*8gdlft91{O< za)&Uz#FsZ)ZV&wI@fq5g0mX`?na=esrLZ7qW(ALT%Vhi9@rV=hKHXs4izH^E=pg^g z*MCl;iyK_&d^Ecchda@UN~EQ*|5B+>!xi4GAb$q|yGD>$zOq@eO`pB6_I*}uJ;jm! z+>>8NTHMNNJt!2RpxT<|o9&WIs7*F#OfnSl<|~A{8y7<|UNNR+{Fdj69a5;S- z7E*(EU?@~t`RZH z@jw;=P`z+B8^i?31H=C=!35DV)>kolex4aHIa$T`#e&@heH&j#AZQ9}`9 zsX;9-(e9@QE@Kmd8D`b1*kq6g038b?)F+i754dU(zhm~ z&9Ijge%eiQ#Z1Jjwyl$`QZ(m3#SWAW0AIZNE-O=iyM5T%LW{cLWjwOuShC7XSzje4 z^^8x#^0dmzA3u4%+rAtM(nB^N4mOCj*zX85%re$63wL$Q$1L(zUO@6MF0`-q%8>H1 zef^CDsx8Pn1@BPAFB0dx*SbGF_R!m1cSstZka2m5-zWvy*FOgoX45E+V%itIi>Uvg zGlxZjWcu3b42SbPmNPTyxfNu(bVApT#*<$VM?O4-!uSEd_@Q$S1K;P|w(H*c@P?ua z`b{~1fYkeQt&}X?^Z%AWHXTcr(xe@=r9YFszD{Euy0ZFIV?|MRLee+-v zwx+JF8RX7e!-B%7VGsLHmcsL$sL}i1o2Fb#;YTDu?ErX_UO_Te5hNC`Jl;}MkZK^< z>T7TBpb50-6P12<`TbrXCd=-ZR>_m6WxJ|^lom%({u-~y)U|eb*>=LG2A%DSaM2eB z%nS_|iP*Kp#NtFGR)9$^xgsVTMI(lm6*lZ+I^h@;<)g&OU^ymQD@*InQDtGrAQIKy z+Ua2LT7gN-U+T%b_bQuYX7!vgsIE8LJ0IAxY{n?7M3h5zOkW>YKWZdmN}h&0Y+p3X zmR_zQhZ~jV7-Q<^wCUW8YZWFwL8L^8MB+HA`*frgiZb^wwkl zYGQR;Z|b(s-8A2dt(|V{J~~SOq$~=LSUW@hyVg zOuk3(Vm-&7zgUoOzX*ZCYtNSGW$Y;T0@K1}7SeDq%=}pzI%v;F0!xo|J{5qLj0cYMaZ5>9JfCp6If(UVqJ-C_fe$bo5eJo zS)<^}+7~schKh+BWogOCXbcJbdp#%wQ7UXW!VtSm>`?5cYAil)WOJ;}dO)0?0SM+=0BUOSMV z5HAxPrE2%h!|c5UrJ#>c7*SK(-#k0A8%qmf{~kwkL@Fz(RFce3McnITNonS9;K!EQpzLq@KgiB{=s* z|2jp1gX$h0%Tn4idc-iNTsp}O3m2d|N$it)QQTrlIxs%ATCjVR)-Z>7NgO$If=CvX zJCM(}#&;Xjwd@Vm@aNqy2d@5*HhV`dsNuoBo;JHQ-hd%=ScoH+Zbxg07Oj;`gz+&7 zADk;NJcYnd2yu&#gRRK=*d+vKmpwb7uNtQK+{Zx%`2>wp6Z zXR0o&boGFu+lrcEoRPichm0+yIGs2+^|-~-Mz5f-pRso%YVfSEeBX*O6C(gqfi7O8 zUEHM>wKmhD=kW6#nutrmKG+x!8I%OqJ{w9B3;x-e))8jWDWIk* zw4vS{4?S!cOSt#m6SmSJVjB{VCSrIeQpoua`!L^*!2asb38N&ubaD!Cw~*}HcAcii zFUCcN>K@zIl4e_w@L8Ejd|32U&qj1unOfU@AHS*;wiH6eV9Q3s*ze9tk^^gFN1hGG zGk_=xEO!^}qio<(%@2&^pB%(<^-;zZ9Tn)BTMTmQ$Qk;wfGu1;y9Hqiwn z9KI|g&bKTYA&Fd8nhz8tUrC$wSxJg4Tbj;O8tzLa$PUiz>Ur#Q+Eke(d~0=KJc$)^ zer&HW`S^ij^@rKwITGBMW3J$6Dfx6k;;td>l+s?vNtd)V8BC76r$#L5*Q;)QN0lFr zKSwP#$ncK(!o!oFJU{WBZ%TcC<<{yjRlGQHBLR1P{IkS+d?)X6uTN#O_GTBrVfgXp zwo?WOw1SrP(|vAAq%rf&Uqw<9pX7PJ$~b#TVyJ~S65G0o`$?fx$xa-9uH_rX#(Heu zdRns(KM}2Nl<5%Hfxr2xB!bT?;xt)A1D$S@QL?L@-x+xeP3lcibWiq~;$4Ti>6#v~Bj$h6*lCIl&+8r*4X*ndKmXvRz)6*$cI5yvBJp zKlx;H`?uzAH;Yy`SC)$D((Q}?*LRzh@gJLxS4-DcJEm!83d_Y=jEX`0YonT?<8=>3 z?k9xl0hF3n2j%81S7+78L?^oq)+mhqIBYUUPaevmw05p!qNrj z4!T?EJqIhp+u~67ZeNE_2f+kQzV@YE#v}*3$HXKFZH`!3Iewe>)`m;daO&)9TB33} zfw#vJ#cTf_z-=b6Pr_K9(eR#5*+oXi#Bc-?sTooNT`YGhgD<0c6o348{u|F<6e{iI zl#@A0Cu>J7HypQBC?%zG!kpYDGAwtuY1F5qHTE;=jx*?uSUhcQfEGWsbcm4Zhh^+I z|ND1P{*cwr@6R*3;eMu-ckWS?6J{=`3JSf2AJAlb8=_IvY_CSyWHgoGQ-;SUV@uV} zCY%@{>%F6V_|p7-WI1kUwzp-zIyt!_nJQf$v>z9oqSh4;g~gra%TJ+znz=H>oQ%gQ zk?&Q;G&K0j{JeJ^rCOx-+_-nZ&60j>KkZAueu~!@=@&5#eKxQ%akAT)ZA=O8xMUnj zD4`We`mojdCpha$*kD*5QN2rKh$)U4Hlh9kMObOF0(YZ`DbW~Xb?>xmfr~&qiu`+AELcx z_;ECehBBRR)pva9f83gL`cffpbarh>7$8G8_FYFni46+59R%~zi@XxD(zj#V*S{=t zvsV4oS>KgzeEd#`E8kSme)92W|c4TWpDrJi)kK6RC%78oVxb16E^Kv77l#fJN)3+9;+_J zKV@>s$P*D5@S>CSzi3y@WK2)YA>k>j8$Y_=ogWqdkKY%=@otWuVH6GZTb5?0e|@i$ znMJ@ZNx96z{;Y^L?rPIU6t-9i8wOcX8tH|l>J<>ORMaBUr%&SbA68Fg@bvF`mtw1p zgksX(rM0{t*@YQ}%9v)LIY@ot-T$-hE&6k4P0?>&H9{5-c|m@!l#Wx`G&{l(JcK&6=BMH;VA7$(4jjpMqQmMyM^{aH{T9z%7E=f3t^n>U+afA zdHIN^wmx4He-|0L4RX?d*IJ$~c)IIr8%(0{j;7+>%R56PsS&T!&Mxk}UmZCk%19)S zTKut@A!ZD3bGuRa2AQ$U8=XuYc9}8{!|cp{z>7?sS!%QTBbe7_Qs~`$G@l3yQI^}u zc^W5h7A6udJwe^&i1}WfJaoN|QuQSBkG5^iUZ^-6o*e8JNS@Zs>bxB4^I4#Tiz@G~e%zy{%Q15fH|Sp0cf)Bt&2I&5Xr$0>gwk&PBQsV(Ua`Imk{%6! zZ!y25rMa*3jG0nxhxZE1{uxzDRieqCL(K;sQ10io`nI*86zmK7`b@5OogOho4JSLT ziU_kHoXJFx3jWK0!S`ol`g$LW|Gm7^q9qk0T2e}>I^B@014ESlU;4$6 z{^4T*Ky7P(#0HPkAJ6I1fibX1T<>{2wIc!tXiSX`&k!0vkPw<}b;ahxAA{-1;LT?) zM3q0D?;5oTtcX4vDO)X2Lnim6dg+9I7$RK$E7|VWf3nI|t){c!O~)LGaA{9(nM zW@qTn*ixg^6vxTbh*Hez?)3E^RU^Mq9tBNOo7Yi)4e8u6z6kP}qJZOD^Owy25avIV zA&Vb%D_+}-)|NE+D*qDCR>kZLuveC1r5+&EkCv#tqvo@Zua3%#)~-`~;G||#0nMmKsFbfFCikbe!we>mQ)ETq+^O&&ydi7Y^3Y^en*;G&& zgHwA_{h#^+NH%GP`Pn#c&Y!r=T09Gy;}Vs7sPLbYKf4Xk2(tiJr?;O1v5Fspa$oG_ zlF(f>0L>iKy`V#$BetJqJOwqWq1pl71R5#L`-#~((3LefC{{m%eu&Tk2J!~nVgJ8} z?qS&Pxgo)xQ7e1E)je93mw z1e=D?>aPb1GC__zd+h_QK@Z`L@;-15&hv^W@?8=H!GTa5e6DTHtS+lT07bp|_3>^G zL`#ZyH(E-$v)3l5tL|?8-2$!{c@9Cit?*@aNf&Ka=uuMn4=-V>oMCGmo;WACQCk=o&kLt}7t*vbItRd0iGv#+#-cdOsUtdH1t;Np*`ba-=9TOKu!RZ9->vPeoWsuf;K7`>ih|hXZ!tK~P6ML?P#A>`>c9ct z&BbO7b#$sF_jhpSfI**^W{|7AjAYDu2pzNvx_hByhlj1H*(4@y1|Kl<^qiRlx~8Nx zRW%Rhdt`iXUuRlKpUG0d{!;B>&xgFsJ!309k89`^?*~5l{1E-AX0$Z4veyB3b+4b* zffE?!AGJE=@h~S7s&45>#qceC=!Cp zo2E^kYqjFND_z}Sf8^#AF`;9wH8M_5EjmOj)dP~r(GS4DgJO1e-f!-)KzZqMsT1jl$2lU4XRC2p z8_P7OR~_I)<0``q%d+Z5hz+`WuIkJ(KF(Mea*`KqCHxeKQZ^9MFb)ADc+R8KLgEXk zTJH-JX|i0OKGlQO)&oC=?qu{qee(rJ0it)W-}&Ap_IzgE{(iIqFV#lV=NGP~`fOq{ z;l)GH>)RU)kiv8|+3M#>A4|_d=;INx$K>U89U}zH6URj?I82bAn@&#~lR|s8XI+HD zMW%3P_qwIDy*~X-uJgHr=`Qk;0x^ff-K4nFVV}=m;vQkPK(62{dV=ThmNF-#Ls_IR z?u{zkNchm_aAaYU=bcbbWG33Jf1YBfONv!7=Wc5a{GQkGzjEotL|u2&i-}GQ zYDt}O&o{h9G{_v&E#>N|;}&fs7@zKqW3;R@;&E^&vUa^@95g_@g?|WIOaPoh5}_9i~erCNwJLbV|O?!??O90d^WSyp(E9_(QHElH4>8!ED9L z8JZh@h^JmB!`_-2;&9Ua7a6Xe1XMOX7Uo9{TP|i9oixnutQGp@TeZ-0@F_w{N#@wq zqDVdHnTy&U`Z+PYHL9}LFXAjpbuHzf*?RKqZBApH$Sn)+ABC{P+<@EXyULs1bFtycbLhWLy0#<5$ZEv1y)lhL-HKx6F$`HIK+xxk@1KAc} z^;g{rIGcPgccE8SB6(Ex_{`zv9#eMFcIuk64UeHPo2)@P&{&vaFn%Uyz65NM??-6ski~ZOl{m)4b+u7YW@x~?q z)1vawGJ<3vaS6<;4vnUM&|~EYiM)wAVtL1DQp<``PR9v?aLJGb9-xrv&PV(-gB3#9`w+O>Ht!Gums#%maFE<8Z~u=(eL{J`l&P;n%*D zv=m!A57<6%9~Kwng^tl%EN#-=YJBxJ*FZX2dvYs4&FBnx%sfcfj)7^al@2?!_OoW` z#FK`Px%=aQVR85WP@RrO0;ekB?NhIgNpgECG3tNYfZ#J$MP%o=8JNH>F(bR4`!Fy(GskWJjC+Z@-(j+x2YY5Qo1|U=24Lev za3Gm*I8)LeI3FyatrsaGUoscof^~yWOg?*$HK0)f7|L=%Hq-BMO&v;MLvsq4vvuDy z3yqWVb;edV3d{Hm0qK9uTy<-gjEQ>AZ|hgR@N_(wR!8sQ`}Q_0b{5_RLq^`BQkvg(X#zy7sL;VsA%UVHOB z=&IG}O}c721keAClbqb=TWmH+2u?QDcR-6ee_d#L4w`Av-1!12FB+*>gX%AG0Eod8jg&B zrqN9J(q^{T5oR#SvEER%v9n5Bnpo1_Bs=Kd`+F}u7@ta9cu#ttAb9*ZLQA|q^mnv& z7X59$Tkv97S37Y+{DoXfXk2@nc>glF-Dq&(3ZlYbS^x8Y09O(mtN;K2 literal 0 HcmV?d00001 diff --git a/collect_app/src/main/res/drawable-anydpi-v24/ic_stat_name.xml b/collect_app/src/main/res/drawable-anydpi-v24/ic_stat_name.xml new file mode 100644 index 00000000000..2f6d1ecb9e7 --- /dev/null +++ b/collect_app/src/main/res/drawable-anydpi-v24/ic_stat_name.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/collect_app/src/main/res/drawable-anydpi/ic_action_name.xml b/collect_app/src/main/res/drawable-anydpi/ic_action_name.xml new file mode 100644 index 00000000000..3eae901552b --- /dev/null +++ b/collect_app/src/main/res/drawable-anydpi/ic_action_name.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/collect_app/src/main/res/drawable-hdpi/ic_action_name.png b/collect_app/src/main/res/drawable-hdpi/ic_action_name.png new file mode 100644 index 0000000000000000000000000000000000000000..3730847f41968805ac29a4ff71abd5c79bded5d6 GIT binary patch literal 649 zcmV;40(Sk0P)TDb`$Z?2Va)#y!Ym1W_D-Rx%{YfyWQtWk{tAUyfk$hA()0fT9Z%63qnSR2s3Cj8Vls*z&uAvqqj#=5d2p%-6bb4r2RlY za)O|NOn1o13ysKKjqXytUcavE&Gg+=A#Wn48E7iF2o%CJ10{ID>al-BP7u=yqy_xY z<8`8@OXN<3$0RzCM&b^Atn6tcHwc=<25L5&EA-H4{glr#xw$~Iae>md)$Bd}zD9jo zx3jz`(99P|Q*yfBX&1kd-k!5HLcK<3^NctaH1q{R`w#c`RJl*B?!Bfq8Z-?B8duUC zdUSm^i>*WV9+L;n0X3TDp0&AI1Tttkj>t{fG)w)HF+P?j>Kp{x`o__8kPzL{Fpl$lOvVrt?e+l}&+6w-V1M zbXr@!X&P0bPicepoei3mEqUH!KT)v7Ow*#Jk^4mdLhv2bV@qKk(6=n?B=}0Z?Dc7dl{|fOgER`RgQT zw%Ub0G5|)Ap9@XjO9Jvh(T{qAp^OFOhVuj8Pr}J_Y8DEpe}UXO&O229^Fx1@#H0U# z!i54l`)2o#fEo0|+y2H&{CN^yl2Lb|dW0EDn2ClWdtV4pWH0a&_9ruCSi!@SnP@b7 zN|{(>t{%#M<*@k8Tr?UUpe-Z3pTb}HxeA#g`gxxuB-o>|0e0xGghXdFSKy4^hLCWY xcSzt1eN991&#D#Gw%xxfifSjzjC20gaSMUHvwWs)4$lAp002ovPDHLkV1j_d2lM~{ literal 0 HcmV?d00001 diff --git a/collect_app/src/main/res/drawable-mdpi/ic_action_name.png b/collect_app/src/main/res/drawable-mdpi/ic_action_name.png new file mode 100644 index 0000000000000000000000000000000000000000..995e2f1ba92911c0ee2f28422a7cc0264b7d5a27 GIT binary patch literal 420 zcmV;V0bBlwP)U-!O-*o^Bu%FNU}(DY zW?r%hyE__%+3NfL0Wlwj;oBF;%}7I9S-h6IuFqw&SWu1Zk%?7>wC?!nH7ZV!1FO12nite6as- z>gGhqfZc{?G<;#r=S((XV?Qwi9(4ct=}B5i$RNwI9loK);E>g=9x=SeOUQs|nkGZj zPclMWuwiC6g8TrZsrxZT1QnbB O0000I^Z>nC9QW>^n${PBqO~lRVy*Q_P%zgOwqT~~{0M@_ zRP+KhG!uZ=gkCA%(o7D7x>WF_zJkn<^5^R3K15X`bfkWcvJFa8wgECd zXaP+v@xHz((*XV%W#6Gw&E&8|1I(*5Zc*+7YH~J;MH*my1FdMjqTFS;Ux>vU1SYom zmk%gY5925nZ@}Ms;F=rwQt2InhCgDbqye{Y+QKymL@YyXu` XpjSGLy?=na00000NkvXXu0mjfuFtSu literal 0 HcmV?d00001 diff --git a/collect_app/src/main/res/drawable-xhdpi/ic_action_name.png b/collect_app/src/main/res/drawable-xhdpi/ic_action_name.png new file mode 100644 index 0000000000000000000000000000000000000000..abb1053625aa894cd11cd58549435250c3270b97 GIT binary patch literal 863 zcmV-l1EBngP)SMMVV_BqBx$8e_bCTu{+BMFa({3O)#;6$PJs(iia)ixx_4(Q5U-b$;a* zx6|8ec5nC6hYSoY+1c6O>}~e;E=rX^nx=E$H*gjFJsOSvjziIAeZCHd!<+0I8#N64 zh>I%AUaxnL2MUg6v$=-{4owVG=W&Ce)9K7&=6~^kp@}8+Q$+|-=Ye7p6Sd9-lmZN2 z@*uI<@Av1p4qp_`J}&uN1Z>L^L+T7`01URD#XAwx0Fy?g0a~rr zZmjzw_8;EQ~Qg|4U~K)vXci}o6Ka6$9{nECJQt%vbT+nT@$Fq;7oOnYGR^_i-|pC#N7Nn4|^PIs=#YAsZVZ zUbEg6@OaO@nmA5hXT2BLIES~ABsmM78i(15^=^*OCUPC#z$`sy^VkgW^OXtfZWM!Z z;8P{H&n=L%Oj__v8;aS^6a0s64eO091hH(^l;t@gBx=|lHoZCtL3rNbfuaMK`;t7@ pf-APYJddIUm%(?~mAdTff`7K@-O4VY$5{XX002ovPDHLkV1mWdn56&! literal 0 HcmV?d00001 diff --git a/collect_app/src/main/res/drawable-xhdpi/ic_stat_name.png b/collect_app/src/main/res/drawable-xhdpi/ic_stat_name.png new file mode 100644 index 0000000000000000000000000000000000000000..275fb82de58d1ebe0d090913fb610158d3e5dff3 GIT binary patch literal 739 zcmV<90v!E`P)hNCL!8uvbpbMOykdvI36b+c>#0;F`+$1P?Gbd%@x3r$~YopK~oD}d~ zdMw>sfo^$kx4ajeUke59AYO$t3 zpRxZ?Kyjy~2iE$q8|Hp3EMo!19AK{JBE8XArUHuC#>{{_^haYE3h4N#^plyMZ_+xR ziN^92aCc*6e6@&ctFBn$0y_GcSplQ;*TyOq(AFbwnJL+u3nD`PNW1Azf>kJB?e8iF zgS~0&g-nJyNwI1LuG1&BT6?jOOUy}%RVz^T6mfTKow&^mht_Ag7L8RaFiBq*7J1&G z^;zi|*Q2p&1%|Bq{cgbro^@#bh_znO{VCwk*0}VG{s8v@)^@$4wLO8Se+3-cK1_eW zc!__+mY42t0f&`;8L+>^=N>#SEBarcK-SCBS*{mo?-imQtEWIOnCoc@{KJ|8-C?dL zX@NziJLrprxt@pgMWe90O!o}ZTNPz&VyI`1-e?qdp6Q+?daI!GgVy=x<^=uGC~OPU z1MW#RgkJyv002ovPDHLkV1lNxX(s>x literal 0 HcmV?d00001 diff --git a/collect_app/src/main/res/drawable-xxhdpi/ic_action_name.png b/collect_app/src/main/res/drawable-xxhdpi/ic_action_name.png new file mode 100644 index 0000000000000000000000000000000000000000..804603a7f9c19c2767df23848f30da8427b82894 GIT binary patch literal 1379 zcmV-p1)TbcP)sN7cZEfvd=Iy$UjYgx_v@rWzxLnAnn>#x@FDv&Fi|Lex!{I9|zBJluH(6H~ zbX;rkM;6-_s^u#d*qT;XSMSqt<`#==8<})X4sfaxm)ivxa z%DRh4Yv(MB=_)#Tj97nmY0dn@Lb{5LomhWXDd86^Vis!ur~ZDZzgrq3J-H}4g+Hag zL-lh`{at6CSt#x_mm`~Ew;pRJo@ab2+2Am?7O~IxEYjLrcapoTudhGO_|(#tvCO

v3872E+%b)ak<6(9$L9SFfO^6w1HEM%PlSZ zC8v%l7?)g3=&`c0azEp8Yhz>M8P?&!xa49&k2bE+*qqREYfJjwt`q5a^OpYoruazz z|Neh(P>fXDNq&}GG{d8-1{D#_@aPQ+&>Iw>Hz+`FP=K}#(wOKW&HPtvp0>5M_2{7F z-42@J(Uw6ryFa|N!kYH=-5IveA}9G2Z5U*CGFS)E7Il?z$;G4|r45of-?D}#7@J&7 z=uy%jO(i~N{wC~7B;%8dNj*v#q+RA!=5E5~V~o!tCiW;{klkWt-X@CPA7gTl+y<%t zhhwir%bdRza)Nzi6ASRjX^>sku!Uw$54HPCJitD(ibZ(LV~~y`8`>H!@be~Vfp0Qj z6KeYef@p!SGFKBNZyMYs7UB_W5aWzN!?ZvKe#5piC^KsI z#eU7W zMX)W*9yezHl%3VAJwHz_7U&UUkoKUL7`F&^QH*_4GVtW2J-9?9%8Q2jRQ0Q$PEoWX=hjZq(!0Cb?LsN3206n`$sVgknBt zKb~aH4aTxfaxtODJO-Jxz#p)W8SF9MQ`5+s?UIWLJ?1sYq_usG{Sez-80Tr$8Io<2 ziwQjzF-Tg{mh=nznxKWgZI?P2MCri&E@SJkeR46O$AShONJe~8Kd{jG8{^jCfDX8If_x#e^Oy44PMy2dF|YF1eV{qc@LZm1Fy%K$X>AY z6N_w%s!K0es!IpMQ!KVLU42DYUB|V{yEL4cH2h9pOaxfR|#Je lsQ#|o8T4@Qm-Qj>{{Yckln={8=+OWG002ovPDHLkV1hhj3n#S`^SiLXkW7Rd)e_?q3Rb5gY2orH&3cXZydCZsO>x^nKbF`9z z9#cKR7f^?|=P2{EM#x@YenaRcTvbhBzD9-Y1+-1#vuP@GHb!A?GhqvyWzI$@$Pp%M zkq?=-RtoSs6SYkIR-41zwbAA`n5e)#)n?Ty)yj&sTD48}S19iYbJs!}zse*z9;?=| z|7Gc%8@N${$?SKhO-Gp`$2|59i_ZNTH_Ne^z3#N>O{Ors$-ZIJ9y__I;RO5KF@fO$ zd%T47Z|;Av#~l+G4zb6J_BzVVi9PNZz_67)UbNRfZcglR#{h;A_IL@|kN)>n(~Vdk&y}@I(8aoS9Z37C~AI|Yw zxaDWnT=uwQ3PUXe3MDW1vdJ?5$&aAOO5-7$c{bwDAzi&APh@*ex$F@eE* zK=JlY8T;5<3#Kr*4M^wyo}1-3#opR5iNR$+I`1BCl;b*kYsEB%p${kwb{%@$U-q?P z8bfkGS~Ep;x3~ULD7b#DV@sZ&x!6CU>ET37WhgVC&(L$9Ya%`V3fAR`mtNKU|=4HYHtd1=jYKIV=o4XFVwWJ3ivd0x_*PvSy8?hZjMg#(I0 z_9MBf`yug8G96zVdN&n$%-xu%wlV`+%%%Y>U?@GHD7^TSqsSKak=Fdqrikm@ zF@Ryv0Zk3X7r?$^4{6PLHa)|&?ij!@(10TK%vSxt6Up0LOIq^{oBDCRI|eX}u*Zw` zTF=diJ?|D($tKn-hE7F@T{PP(nALgl<3y-GCCh0VV8Wj~DHAked^G+%bUR zYxa22UMIOZvBw<)7#_373pX~HcS~Gp(~C@D*vr0Q(;i=PQ^QZ}a~DdydAnyBdxu5m zUct?BH1c7VQg2-9`IP-HD-3oGsqoUQn^16hA;*uZL#mG|R{SRgzl6bj$K18i<_nmn zW#WhVW-xcH6yPvZwaB~7+Xw}@#bhmTjyW3>vVUUICho>mIkPq(sfv>b*nR5O{Q^%Q!t>K)auA?JQo1@V~@|5wOq)mz+i o!VB5`ulOeLD%GwR-cBd!DOl+tAqmwo@&Et;07*qoM6N<$g0vwtRR910 literal 0 HcmV?d00001 diff --git a/collect_app/src/main/res/drawable/ic_launcher_foreground.xml b/collect_app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000000..85864291581 --- /dev/null +++ b/collect_app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/collect_app/src/main/res/drawable/ic_splash_screen_icon.xml b/collect_app/src/main/res/drawable/ic_splash_screen_icon.xml index 2ce74a7e35a..ed8f8f6a8ae 100644 --- a/collect_app/src/main/res/drawable/ic_splash_screen_icon.xml +++ b/collect_app/src/main/res/drawable/ic_splash_screen_icon.xml @@ -1,7 +1,7 @@ - - - - - - + + + + + + + diff --git a/collect_app/src/main/res/drawable/kobologo_symbol_cropped.xml b/collect_app/src/main/res/drawable/kobologo_symbol_cropped.xml new file mode 100644 index 00000000000..e17fc8e5030 --- /dev/null +++ b/collect_app/src/main/res/drawable/kobologo_symbol_cropped.xml @@ -0,0 +1,12 @@ + + + + diff --git a/collect_app/src/main/res/layout/first_launch_layout.xml b/collect_app/src/main/res/layout/first_launch_layout.xml index 2db1b409924..2314b237587 100644 --- a/collect_app/src/main/res/layout/first_launch_layout.xml +++ b/collect_app/src/main/res/layout/first_launch_layout.xml @@ -28,7 +28,7 @@ android:layout_height="wrap_content" android:adjustViewBounds="true" android:contentDescription="@string/collect_app_name" - android:src="@drawable/odk_logo" + android:src="@drawable/kobologo_symbol_cropped" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/collect_app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/collect_app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000000..7353dbd1fd8 --- /dev/null +++ b/collect_app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/collect_app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/collect_app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000000..7353dbd1fd8 --- /dev/null +++ b/collect_app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/collect_app/src/main/res/mipmap-hdpi/ic_launcher.webp b/collect_app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..b0bc443414e0070010144068f850e64c8999896e GIT binary patch literal 1084 zcmV-C1jGAMNk&FA1ONb6MM6+kP&iB|1ONapN5ByfHwPh-B*j)fkn!XDH-kEu`Ao+MS)DS_3xmEvrv$l4%Qx1NQ5LgaqjtQ zI9swA+rIo7&7zw|1tT;V#xeoeK#ln{hz=0N;V<0k-+6_ zUH$$4tlb_qy-(NU#>INJ@K@jB=jU+=pWnY%{QSKGexUpN|0%%R&3?6;1+f@%2?+@w z|NUEsQdU;hfB*c|7Yt7RH(84zJIis5FUN7To~OOn$7K-$V)6%>{kS`B_Z8BvZ8IdN z*Rhit#I|kQwylkA+jeHt72CGGh^;?TJKb~YjXplLZ$$qm!2185H8V+sAd4XiS1=~y z%V#g1Yt053EtACsOs2=#jL_dad23~Hl+~E0+2ToN`xtIzb&}b9&K567W`F(5Vn<=0 zsN_gG9Of+igCJrO4~096{6Zy1;;~2ohuw@8&P%iyC2V_Y1wAQZTdI`W@JURH3r3zM+(XFQhuya6B z;jq}i-4Z}M-R3B?%T&3i5lXvCde&c2q&}?-`lJ)w2 z7jFM>-j+kh_bUDy$TfI}ggreEIbWAu62RzOuz< z0OVdSsRhj|{{c#@Qu2pnX+gd6E>P;ZX{5~a>xJ3M#N5vEOjTwG&AKXqVP$>6{N;!5 zQ#$HaalKU6PMaKb`Z}quDW@Ac2Nd^v+6e9PjIu+)LvrhfT)k~~aXoSh0`SxwyUp3w z*qRlo(nP?Cxf*rz<|BMM6+kP&iD42><{uN5ByfRfmGMZ5W3?>>UgdF#+Bc(L}p> zv|eWAZ8iUY)z+gGvabp=Gcz+YGcz+YN@iweW@ct)W~NK$o+Zw`>zuuB1OEmhqp%H@ zjHiUe-3FrY#Ge9QnF>FRBh_R#YF5oRgOS?NAE=m*=V!FmOwVfN0yc>BhEg+qP}nwr#h+VB5C!3vp80?#-D1In}m3lj7e; z&N=6tbIv*EoU8Aj_x)DFTA{*4Y)1-ffS$ARR>kxf*mh+6?PKyk)SMBy2*CgzsOF$! z3}9d@3{caL0Wu{z5I~0t1hD-eC)>dY2I%J?2qCM2)a`kDDz^aukWKzyZPvDJ+qP}j zVE_PRnxAdk%(iXU{|Nx4DXm8764W$E!WyNg3>Gk2x}_xvS9igeGE)I6N#zgw%}$>K zUsre4X6XPx`Kc`}qW$!c;_1Ej!~3(==HB~{{q0Az@TPkKK)2MSYXDT3#?v`^;{_fM z%$(m!XJ2&{fTpX(asj9*?V^vIV?6Tw%lLl}a^k6mVL0XnQW}VSyG>sN;99EC0jLdK zr@(Oo>7d_^+yIfk;A;T7a;^(hmW~UK(!c;45|MxDr{KB@QAPloO~1Gy4ctOQ8c4q{ z0SGG+2FmSS;OO~7#Y4!wi!X$*0%4#Ul*B_0s?Z6cmlUH3;ehE7=lnVzLY0Ov{(C7G z&;t|(Pk=y)s+H)0#eyLmkTAqK36$vX#vfe&K~$50lE=XUZV(QD4lx?9Ly7Dg0608V z>oR3|q*W5=5=(;J@@VV*$V}P%10<9@=6hBh&{-}}u1|A`%w|v5Nl|S=u~#7Y3jqIg zdno=&Rj+${w#kB$FHN3;7k=_Q3p|9(D=)MrRSm{IqB8`2zh8YL6b#|Q%Y@b+9b&V1 z2$`23Sb=*ey32y?)fYm+_`evKqX2X!3!-GapXjhvr)ojSS6eXhX(;3*hr*JdlPZG6 z<{9N7WS&?HzeImH@mRQ$6buFDo3dGSh_Vs}GP{d>eNltaPid&La3evTzfju_MN0hL zjUPxsN?it~at1vy7vk2qWSc$Fk<3jAg1?Q0Z0!q!{^flj{8JL7hyNsr^KRL0A{H!T zB+y`++Hs;LE4N^qG5kdlAIT6)!6a5-C8R@i2baj+({l{k8zB$v_0V8^Es{_MXYU6+ z0T<>KjK4>R(b4b?y4s$GzJZl#nm3oz_C`>jE#hT|fw4hz#3B!UNr3C4!xC{`Fp+s+ z(ADuY7`ticM8Tx#p!-;M^bD8&9BvU;;;c)A&H?}p#^Li^p_7yxvA|8IqM!j-Ft05) zP&MYvff<=Ai#MEw&u1FN9Y$pU&|#56E>$xyFfw$DDCQbYK4SQDMWp$bvq8#aRkjXb zMV9DC)r`*N@1ph(OpXjrHyW`((+GXkzH079b5~KFi#LMBXFDcLnx;XyEHPL#)iHmi z()CMGW4QC&kp|Zk^WY2N1kHs$ggiIssKU(D&qa-9FUMdzVu9P1CBSuISQ*cYLMu=7 zW)0Op$8mCEcDi{8n_yCBpQ38%=Cf1Ew&$J=ob$FST6?$I`1cq%7CUpj+{|g zG?iF+quG`z!MP6ry?y&pZ`bCSVopK^KF~CcurG0dQVGoL*&Q2KM;9^xcp~YWuq2=1 zlyGSpScG-{L50v(4SyN9jsalyNXJW#qQL_bPkg}w(65`Wf-8jDOyx*K!ieV!0C<^EgwHg_`CV8l0VQ4epg7k!-DP|%v`zA^@pFx~>^s@Ys z)Xl(0k~rZ$5nUD%O(&dP21WxnnC8yZH^o(zs9$EBetx9H38#I^Nn#;rFY^7}GbRT0 zvJkPUXM-Uocd@Uhh^x>uxMvZpy#mkRNYr9~&r3Nrb+n}&mIuZG=-*$P#ap?QHojKYVrQhx5589xQIkYaSI|L zS%3`y931I2nQCdcQ{?Smv|Z>M+<3KTfhxsZsLf;|d4(MSyILM1(`n}XNE{nv0pRto ze<0g_FUY}=cwa}kpRa<~hCoKjz zi8#^dxpB7V6mjfHN}P18NSXDyyB4DPuVZXb(k_85A5o%Ip7V$c3qTx*4a`898;Iah z!!MTWvdXjT*`V+dE#Bo#b?QQ`ZJ)syh+)QNzW&Z1g*bQvX~rSbG|dZ5xxv9g{Ba5R&=(tud@ecbdN!z%(3isXY;u+& zY`d%+ZFR=;e`iWXktk1H8Ww?>&PQL_46-CYD|aI_s@*NENZ~7q^6vRrwvfXfq`Cq>E{A`!gVg5YgWbf^Nf9km(XWTJuH&At?K;~_;X8tu%c`ygv91Nc- z-Kl)nZ~XkJ91Qk&@MXWwoORo;M?n3NBAC+lD_STgky|on{M+9$X1-;MKkb)xa|DvC W9VwTg1jfpk%VjzGj&sk6*q|^Egrczk literal 0 HcmV?d00001 diff --git a/collect_app/src/main/res/mipmap-mdpi/ic_launcher.webp b/collect_app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..2d49216c661b084770022b9c55ff3d912bbfa871 GIT binary patch literal 776 zcmV+j1NZz=Nk&Eh0{{S5MM6+kP&iEV0ssInFTe{B4+S}rBt?=hOSszam!5l!U7?2C zNRkw(p8a>}?Rb^MWBQ#gw~-_%QXc#7->D=rJT|P_wv}b`vuCV>XP^dAyoVWi_}{*R&*fUeJd^ylZJ|MVFpBaWVrz zZ48w{=?WF4Wh#lP@!Z9ushH~TGT%%?l0rl!w=~}=Ashuq%4JTQ=KIwW$sxXxds=fx zi^OuAxpznp?o_fd6Gv4qGY_?<%evdyI8t{o^RX;ha;*yqrR~0OI8PRy*Jy~WA~K9A zh#|dGbA5~k=B_ND)DF%2QzH1F=Jr$|YSa91TSkc8-mdwqglg*6@u?9^?d^-zyGwyl z#>OiH5p=mFUBVINm(_$;m*r8+1&l3Vk*z;HAN6@0$+s|v=0^R=ZPmCb5Y~`Pyzku!EmBtm(=J)ay;AC78d*(ej GiKP+%M|7?L literal 0 HcmV?d00001 diff --git a/collect_app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/collect_app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b1c0b8a33dffeb80ca1e71c15ab831816fa07df5 GIT binary patch literal 1578 zcmV+_2G#jeNk&E@1^@t8MM6+kP&iB$1^@srFTe{B6$e4K&FZiHoFO76pbgb!Ogi@e ztG1IpiP#ui2ap-KC)3DEnN(S+tao>Jcii3G<+-18HgaWOM)1Sk>2xnn8QE*S5-IJE zmAsZ}Q*-I`Fn~ivwrzW>v+p|Fwyn*YvT61IZ|{A!ZS8E^wr!3BNw&>e(utjX+qP{Zrh!N@`UfF^ z5zpHRK++t}ksNIh*SJO@#T>y=I6fzFK9`3--;U$!C>4}v1Mqt1k3#E@1XhJ~lU)y(Bn)7WcqDxi>Pw>1F~B4g0aHNT zUVQ?2k_eN3f~^sNC#x6(n!#ZbP=Gah`^hS-Qv}-{oZ`$tvTrZZdt`gMB-^*`$yu~u z^$GIEKZc5@Vb^)9s}Sp2Re@5S8xv6n%5)8#3IVIrK*~T?BXk@WrQSEu6Ie}Mv&LKC zqKiI4M`O3dSn$Lv`8&uTZj1;hGm(Tia*+hdb#ysc`8RqM36PX+VF*OW(@ zO_2;y{WSK<48)h4LGnM6W%(}2KeZGbR1FOv0w!Zpa9q{qY!pCmDs8-I!Ed~R&U*ga-}edazk__*LO1zT^t2>lD` z#zqo7M;bchgrR_iq@pu%TOYgx*9ple)5Eqq2MX_qi=K2~TVd&yh{fJjT+ccI_>&Wv z^WX%?5arkCRRmSe^*|Z0rLycnMBgUnyr0Oj4y`INHZqz)PbVWCW&4imye~)a$O1Z%I$CuQcED65HRb!qkYP z$-e2Q1iH}+&qO>rMUw7Js30-6i2E7-wMgUB_?MzN?4kGztoc9U)%9!0FAwpOH^I5!xBSlj{bQ2YV!T$fcs+~ z8UXC4#{Z%i%xU`AH5lwx`b?n+S@&3w_0r213sDM;bodU*6NaSS_8Aol`i~_wm-v3H zm;cC};~$D<W6U??-`aZJBoXXPRki658DqU! zkg6Q8|5*bWA^@7<7BrWEFMI*}NCwIbb>Z!~?fn!?Q@7-jE51To%b;LEm;f#~`nyNU z$}Q1|tr=}b-*q#>gU(+QrF1ow&f<8)QE<t6dg86& zJD`f*yJV8PH;W97A-TKz`+RqNo|*SS(_KlhZpn9> z?>6Rb>%O?GOLCVI0mrtryjl}d=JNE~Y zWoHhD-wr$(CZ8N{j{4@Tqbpsk^%x?DIn6(@3ay7PX+ml~}+W-K_CjYNCYumPM z+qUa4005#Dlx>@{ZQG0eSL3#AL?dgb^vV-xffXVVC4pZ?tgez2R#$~XG!bG@j7n6f z?B>!wqN}3sC8T2>Yq-NJUM{NY$;IREHcry2;2n)PM9={P8ECrUkXh)uN&`&YNMm2h z5BrtC@<(lGT*{$Uk){d=$~XHzBZZsndmL(u0{xSVJ=pFzCK+j!8z3so>@HJXk3`RNet;~oNLA-O&bz`7P&qcK>U>1{0X5fb?1?t? ztcqHg;+w`=*D=ojsJ?Iei%%BJrKXWF`GZjSQk1K^p(zW;9Z$-tv4y}C02UW&u4u*v;*KV-r~iO_ z?F-qH__3jQk>AdhC|{&*&nnJfVY^|eM(j;iCC0LmuCC{tRrOF!%d>Qvp|T$O^H0bf zOIoL@Y{}j!17XV3*b61yj6CrW`GrAp6CK%nxfk4IeFv5V(zzDj0s+E9vVjZjN&bKY z=YoV1u08_Z%;$?-?Q?k@D9AR&d}U|2a6?lUBxg_J*MFqtYME*_P?&3oX&&nheXDt* z-))&_Z_@nZ-UEx5`dnSPvnm^5lws$`y+T#zl45o2jp8+QAtQ7L2QxcEMX>xj^1!hg ze<3lZjq2pay6+~waI$v>>q=EKAe_Er$9k|_(d-uCH1z{-nSdibG<<=}<=VDeQml@@ z5umYedfw1u_sEn)O8*6YLaU<^#>Px@L}H!glmtx2$wzKnjx%addH%TTX#Icgu2#(5A$9CH(W+|Xeq{C zXuMJJmOdcPvXQmC+{#w}}V#XH0~WpK0+b!Fjn(xMK-$M@(+A z1MHOi?!EpGSQ8VfJOkeJXZ(#w{?}Xq-gc}c5vQEt0`TKsB-sT936kB0Z@8}MKQM5j z{Dmh2S+Txs^HzmB_*|7uxU7KSc#d9*slOA{2-Q&U8+^8z4R$`piJ-{mvmw zbGfe%MCNk%=c20`op+(~>~d|<0g;l8%2B3TP7yACh&_sP%QN)vg z0a|rw3UuGfzZ^~de;9=P^5QGv2V`G&<7^T>u%8E0f2w<+{CrrgTDbdx5{~5u2!Aq& z!cx_XemH|50c`*(m!Y}e9+cFsp3b?z!-=i&ik zRIYCH0Jd5i6{}Qf?H9ObHaTfHi_or-&fvvbzI;)6arc6t=O{Cd?3*LuxjvtIVA zVqGuS%MR*gcV3M9<-KA=NTY4rc&qmru~WBSZQJ&x3kZWFpy3?vp`Q#%e}q%V(PN0jnp`4Xf=dQzG*AuMb;mV~ zKlKW+MvUpS7eOR4a+q`r9*+8DGLVRd8UOUtZz0C0W_Lv-(@BNk(J-T;aFp?nkQs=( z(n}^RBpUKu@H-qvP0k%m7?+@xptTwzNm@^^D5I(<<0oi{7Ogp?J>Kj!jIyq|gY)#g zfMASDv!sWDyLqO9c49p3(d0OVV-#-S`5_#KsUpYWX+Ia0;3($hdLY#S^ZuHXyoEfu z=1?WDl7Wn|47Qa;b-=hkoVTFE50wFPa%qMR3oXxgENVc=-DncJDdD9ON5`DCe#=U` zBU2mwhLd9^qFj{FOUc`*YX5*;BG=CWi43fhK$IRRs#bj|nM;d=y0is=c0D_AIH6k{qo6HJ<`k22;q;66MrEH+l*x@zg3f-`X>#w*7)p6X`|H zMkjqqw~nXO&0wl%0$sP=&y>tic#!Gwp9HhUlB<#{4``#(Vfoj9>N+wnFio)Foho0n z(?Sk9JEy8X6zO9`9$0X$0%Kva#8bxOEGWr?ICX|k*3VR_$@r0}Qk4kFK$H2CI`(j2 zQ&Pp)O0N&?NIp;X< zF_E6J(*3viA}YBEN^iUy3(}_!zhoQG`Z2dbE8(%h&86IG2nU-(u*+qsiv7@_-7Nbq17man`dgY7OT;Y5oT~v=IJc2`&ZyFOTcK722 zsEqO5q+VKt70X1ELD@P5=V7HXL1|}P>EcYR)Yp%{48X>Ar-q^MMl>6etKyNB0wu+% z9?nQ#0f5OE#hF^5$9p2sVtxJB;l5E}B)ZC4naIYk;2;|Tuk`LCXCS-uW?C2s?}U^Y zGaDx;ILcm@Jvb=KwRY+*rxVlsRkS7wNH$5J&H700$8mzflf+DUVyWE&iHpT+a@a&( zM@$$s$n#{=B4@3;B$HW@)q-|{iYKG72BL)7!ugeIjo%^a28dICQM)e1chZ`mE8L`a zWKBP-@)yg&zHpVqO1E=^y!wiV0w#+Xa>Mv1&wo4m0D9^foZfwHfraLP zz(RFEb(JZ@m{|hH5h*bEl4nOEW%tcX`}9ulpm<*{z43mS$S(A_PbTsU&)~pwPLMuv zEENoxVwtY?Lob!R=nsM_XByS}e)*l1y)w|l;BG7Jj$ne#odYHho`^p6MS*O|8(gL5 ziiu2k^!Z@YC)a)-WsFDtX_9%INrq4(OC-U7tN%1OEoUSQm{y$f)-p5HFvUU*&3Jw8 zM<`cw6sVVY#&!xZ+~DEYne07i-h+O0M}l;Wxtavmgcez1W$0t&&%W=^@6NBtI;lX> zrSJJs5_9EIH_C{kBsThuxlp4wXGwXE=ljQqZLy%9>6qpOFfv++zI18;rQo2Tau!am znGwL8=u=_ji^qN!F+ftd*TYh2RbLBEV_u0`GRdNM_$_|jTq20AOh2=&g2ILrv0O!G?kw%FMF^Q5`*f} z8!s@`a9&2v0-2}4@K^77F|b9ATs9sSXI5}rV0Na*>-Z%ZbIkAnw1bBsVu%7A7F_|o=zUlE^*fEma z^Z*^^L1TaV?OxfflGrV=psm$+iApURygbD~_`sZ(%USDc{~#GrZMN!~0>9Fz&~BC1 zXix^`z0O~mS2E#@&20S1nA?ymF2;AdY>%A)L&OJbR)fuGCsRPY2eKc|t{HWj52xGJ zJ5l~*A+Z7scK(*XJ&201D`ZO0g2QCbbHbc;K=YQlu-My{E)9Y5x`x6VXXjM*+5jYM z*3C(RQQlm!cKNK0%4Ae21`}|d{$~lG4JcfZv(W*0>2F0;im<%&y0${V1b@Np-+wg} z|1lk@Nw$Z}!yAa^A}^sR0@wn}<-JEuvIr^$knGx2VA&fe9>6F9%z58ZoSE~=7TCRj z{>dL^+n9ml_E)e1&nbSLE=)}gG*cwOxPQUttgpA5f~9P$Q3D*fkN0v#5K&Eq78T3G z$Zx=7n?trW1!JjFb=DC>oL*uy4Kf{#b~aO6WORV@N1U~xe`DHNfua;EaPDI9Y?VLo zCMjosFk@8*%f9vD`+Ze8l+q-XT7qNMA>+JX7QVPvoSLJ>*V2}TQ9o7{R<2+qxp`r5Z76<(d-F7XH64V9k7p@P zXj$>Ag_Z=b41d3Y#3Kq90#TrDayshSa#fXBf&Gj1#0_>~#)4%b#>HBW>11gEhiY1u zV>)XIkq7QQZ_tZ%CA|3PpRRaT){U-{y<^#$s<}sxaQSt$Zj5(x*4%s0lR}8KGQ;tB z+%&!Y!kKFcA+*?5b5?KhAD90Tk8XPVj@dSx=^5f|;UR^+~5foVoY6z`1a-r*?c$Hm5ocu=iJBS_=0lxVYS1zD=ig zv2KtvW4@UD-^^L_lrPY*Xv>oA7ZzWz;Sc9b@o%i>gP!SfZPTgyf9nHae{8Y)&sX44 zUJfV_UQwtb>s!F#124KfT;5-(D6G!d##37w_wM_CTT$p29{#|K;Bv(+FBB?Q^{0I! zHmWS!$liCyQe+o@vr@}r!MgCH;BZC3f(5U5wD50_E%wceAKY=@Y{SnBfzqYG?D(0z k%2z&hu~$z;_WI0@pSv_*EksJTVE`7%_9_xF44YmJ0iZ#+CIA2c literal 0 HcmV?d00001 diff --git a/collect_app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/collect_app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..e721b73ab0075fa8c0d4a7276c545e8ade2f2403 GIT binary patch literal 2106 zcmV-A2*vkONk&F82mkQ+G;)PY_g^l)l&aVZx0RWIq{$Fj@ zwr$(CZP#G{0A!kgWuCF#|AU?9he=bzyT+{SadY zLLdZ6pb_+(IKc!g83l$5lR|$oX85-gQ}}1AF{g(rSR^`%YdB|-c7ndQFTScOq!JDV zm}@4P(@s=GEWwy*ehG&c*!q2Tf-KH;n9>1G4>ksg>!iXR^N_{knE6y*%O&F>mORXB z_;Q4kC7dnzZWWim8?P_NHz@4HMo+UvM4P;?BNqd3{iBiiEF>Bi-59Z4a?U0 zJRDtwz#-((~ z10!P&zT=GwSsH{Z*ruxMG}gn>46%a$+wjwQtI`PCq`Nhx)H?{J3u+(Tx|m`R2+L1U2oU7N!w<%7*JSp z@+r+7iM>P#+@=4S)~L^(jUt|+Hv`V3(q@MtUr~9}%+lybN!3P_-hCk~#PRyYXyppY3 zUS;Q~f@PYO=<9VH8~?`?jHZ>ikZ@*)1>F|%n1N9@Sq+1}EKKWrV8#gQXUkL;2RjVS zF@dJlI1K(Wu)4obV*p*}N;LIg;Lc%hWDKC!mXBvI44izD@Y{NhfeHM%*M=wjhQ82( z>wsVT2&cghy!%SZ1z1V`E(7!1_bnZ#fj9E)_4F?Sqd~S{;K9!${~)2{NeXnCdEF}q zo{bDlO$_!tgwxeX{Kd>%SUh=0>o_v}kV=j*$*86~?68b}nL)H;3IogP7ZzG2lv_uf z9Ozj6PxLAVE_p_1Z5JGyTTHQM;F~QZytWmcB?5R}=ZQwbMU@=mP9q(ilyKV>QTVfM z&mp1rwT(}nOB()hGaRM9@s zDx;iX4tS!pf3~Q=tI_5Xs;wx$%S=^TW;Ff~MqL4eapa=nTpfP zXqOPqxzlYf8%8j*Q-W$;uXu&_g(ozIeY3*As;EJ(5rrM17CUYg?=9IVqvm zfD1{O5OrTlNr8?X&>)b0r_YWfIaNY*v~Aa^lc?Q~rSE>*`RQ@&t)gvGCt2$_G%S); zEkxuoELR#Hl2|zkocO0(%1NMxRUGv>nl65jJafLQm9#2c;}vFWc}(jb9UiiH9OGX7 zJX&s!@qpzg1ohI6Lr;>$K(0Z*1JJxvF_abu3|-j(fh39^wgu3w2NafI;5qrA%;;Ts z|Mvtz%+4$QU(y9%WEF4>?pzwMww{^~^9eI4%uY52b>}YJbmQ=WB!dNfy47j;X0v(D zBIvQ%)?zf8Z|_NO1&I#>RkeWBZU{G;8}u>@|W|Gqhk)J_M`5F4*f7*OEb?NZ)h(B(QgQ`|gfcY^*VkKgwas0lKeuh=yyukRN!>K5CZR7e} zV59N^9o`9ZvQpFA)fJ2V0Eaf%^3HuhH7aQTaK?qPb(nGE82^#`08v-` z1`K0E>cFKU=LxR*0rq@29iDaLLQ`9L1SXLryK4Umo{Ps5i~r;NrF_ITk|d{!`c)bV zJdFh=DNsnDZQFFK(b%kQw^8kH+qP}nwr$(Lwr$&HoGrm^BuA2zRhwCnX7^m1!M%WL zwg2y>sQ*54&N=6tbIv*EteE-S`~Ch)m;zArKo>MXDO3P@&UI8Ul^{Wl!qNhY1{eut zV1FJ-0aSpa3j9#Oy>beo2nqm~aY#C#i(3V;0cZdP4k{Q=&^c`Ch$>h=TL5;G+Os`m zNh6zFUb%A1m@}THwu!aTalV@r%54AuWYhoG*sN{awr$&X7ytm7=4abBvu)e;-+|jm zisZUEW*F$b7jWBn8$=I6a6*VAb2@cOKFFa(?ry56EX&J z8Iti0nO+H6Z`8rwXw=?lMQ?N@6GgQ;I9QZv{l<1rH0(Axjx#ygUT=a7hunlzNOKZ2 z!D`Xs4HPtss18>17~>IN0U2gubGh`Fb1uk!NIu3`f=+^3Z80L7MIHP;9^>NoIv2<3 zY;27nfsp5r0gU+w(MX$#!90&2Q=}a>2GTi@k&yow;|R5pW>W|A7&{=NIL^kZKxg7O zh(F|)G_n{OFpD(sg9LD#Nyi$92x46XV~8%o$e0Dc591;`;W!g(RGl}wA-xzIjEtI^ z$Cuu-jvbWdTa~$M2#_tr3EHE(wF&(oI46@cAZIC57Kxsm1L!d}!o)!+53=cTw|3~2nOPA(%93CYME z9XB$+5kz}?*2u>K_>*FTzGH&j!lvK-#1t*xHRdL5z0hg9AnQ0m!bikC*>NFL(p7iq z(GvOzIabiuOTJ6b7q1a_o}e|p0SKIqCQb0>6n>`>g@HeqSC8DqqcFu&zAtKdv|WT7 zXqk(Gji%{dOvBiM!uC+d*b4dzTdd%3q_^MuLP#Al&JVINkCx#!E;rVYPPAvfhPB|Q zTM27I9pmObOPd?EO;;m0(l9{B2@+z_Zr$~3(TTkZbG#JW<^xMQ%LBAT5`c(aV-h?$ zhE~*irrm_2aI*w{{<yBp?QyaVk8z4;a7N^;ZFKE8^jreI+&|GoXrA|6>ybYGX4+=8Q27VxIxXQ{O zy;EVIn_)Q7h(h_$v_anR$NJ;ABIa?K#oU0n^5>(9lEl_P158xZIn7@Dz-lB48i61q z{Xa;N)B8a@qzA-Q+O=sbof4S|SJ2snHu1q|d9?xFL;28D49TyDf|6ek6z$?xK%ERz z%-PHy+(-1vSG`8u#8b!hQ103=T`uo{sODIcd4hrTbI5NqZ)!iJ5|d|+FT^iQoP zW?jQp*yE>Nh1w^Icrq>vc>tQAlb>&$Ak14(k@JOK+thI-l|n3T71YL9v)tnA0f>dT zhz&+sVVB+V0Li(lznG2;GfVzRvB|6K(* zjn&GGp>pHgbWx)d02B`rl$_^fARR!bK03{*agflnh~ZhyfE9TgTjFLby?)SD$#zX`|`m9FNPiQi{r z7o&b1H%IDLw~!dF2+^IaI##%LL_t`a?8RPa*~LLptsQ~Tb;SjJZdTri9ccdqhmziWMlE>zV=j1#Z5Yb=#R{n|k4_G%W@j*QZF> zH;Szn9gNbn=e(4=+j9xgrWs7vBV0q=oY`ybTvmK?(Y0LM?PS7|;p|O6Z()v`FNw(F z#CT&EbC_#{n-%iiqS$ncn`K}%$f&3q4+(N6$*o2*FTYhywV|b{v}|bwesPDF*9aA- z=25H>|1c8o)mDvIf|5b{-td?ViK}pKIWqICwZ|X$aKR+27ErH|K<(S#siIW{Y+Pk4Y=j!pm2!d(^3I2(RLsUfG~Mz06{^LnY?fQ zs@En#LqRjR3N-K7|gxNy_@Z07q9+o*qBRzw~dTf(?a22h!#)OC3 z4Kzr2&{@bV6$XIU3bz@3?wVPyTU@w}o>tEjeNT933ENqc!-a5eHU3SwB(@AT%(@T? zmIJldO%mXZoZEUbC|CaSF5iZ^0oG4btm|hnTjm*UW%voaPMl~PBD6>xu z-~o3lG^RR|H$G8(8XBgHlmCn$li>6dg9$LoO*qf+d;0Uu!~cNr2!5Y`7C!0{4U$-P zk}&if3G+f=0rZ>U3ge#}^#7r~A@+$Yf40M((|(%O12{TO><~U0L#gU{?C8f#!F*r@ z)z4x~v#j)qxp~JvT8T?KvoXQ0a7t_uY>>@C_oqOZ2UNcmBJreC0z|fbu3`){&N+!V zBRz{I*zYU?1(%y39DrGFV)-bgrbGjz11xuQISM%atY8C-GjUd_(Wu%4$+(|1GdOyV z0`pv9IyT27HE&T~y2UxrtcnGW*~2nZAxs%6fCsS7O)ej$yok?hgc5*5?5hMyT(gE> zF$0&`4f=V@JgPVg1TF%~1i)~8jPpdvTa=Y}9cO?6z$y)o;?!sanPp9deMYUw6 z7n>qnX}7$ibqW{(Y%}u>(R~HEOOHKol)i=QhGGYVL@M0M`#xmKp#3eOaS(&`Ropm z+^0H9{du+UDzRlfCJ4-wnRGaM)%@+{&!Kt+;LKr@Mi8)U#b*F6|-I%;c*2dH!&wT|JcVtf1()sDuzaE zp!2%mIFh#~q{CLh7I@dolri@wRm&gz{D7I@-8_vNl$*oVxKLZG|B0=bBrVSyrDggA z222ut@?`mg{SQ8H73?o{WE9tSq@C!SHkOeNkDLRnn^Oamy+Y~OabN*(PD@;cV3f5> z6IReSuNF~SM5&rd1Gt#7=m{U3}2^umQL>PQ(F_ z1~#twkJ8Q51Z}{qj(DFt2E~)S!YvcVWZ>!GpP7adNb9`6qL*;@a22T)1`W_O4MGN< zFs2ke3b**3v-}!Z);wB8YT$O7aMy_Of8cYm!M2gM)=PtS1*|VWIc)r7dr%~+@1(K! z*$NkWdLPp5Vek)J@Q@yu+#V_I*|h0IVivGLZunf+*D9-_NgazJe=O1+K~(=!4*8X)@B1|yn1KigQn@dPy1 z96P^H5nE4HlK>E%aU7WSDx;w~vo?18Sd|Be8zZw+gvN+!M?b=4lJ^VA+XaQ}|6S!n zbB12Gh`48sm_p}gN2QBYiJQvNA45|_UWkOA??Hj8dtALlfn_;HWs9{ z5!F0+pGw0zRZPqUsvvGGc)jCQNxj<50*wgfuwJ7ljHAz35LeBI93al1VOtrW32crp zV?pB6d33K~(oOS9x?9PyAlf*WPEgvwvRxrLn)Pr!rsUm{WH;Sw$lHw_n+f6u@LWDC z8=D!Z8o zTvhFeJZA!?Yah`>dgq;C_vnlzy?4;v58efbUS5LDbML zXoxxZo+(jmk)LyCnpBOBwUTErDr$Rm^ffjdCyHl6;G&pc1;0};IM59eiP1FAK5|@3 zAUT6u=FcKn7?=?GfW92PYRM|&qF+)#7ZXJ^K;XQXzG+@GM8l;k^C}fxf~H$hg-d_s z6Px7aJed(Vz^S?bSS9(%Rhb>>UoLv2apUs&gusasxHeAKL|Q5a2kH~63}Uw$t73G{(#{LkzG8#Y|xNemyF7?AbO++Obm~4AsP#! zz#bRLq787X1`->m^({SBj>GuK27^J2iXu)jb)wo}I5NhX(yfB*;662yaK%NjWGr|G z#ClZHgAmk)e_wR6{+am-$3`Q z++my%lZ=mZ1C>YbDkn%o?@e~x!wT3kEyfHO7=DPz2kx}5j>}P0urL5nLPH#70|tIz zWIHiLYApE8iK)wH)7m+=8J;EMSIs2XM`6QMH$g*6+S1UOj_&kyr=>F$ZB5b82vzk{ zSPRKj@@q3ZTL));S6Of5?#7 zAYaGrI!=lvEUDw$Iedd+Lx%LXYV9sPm7+W;qSR#%)5cq^p)zK*#%sguE~1t?r);If QIH~CgllcJzf$3Tf03eAA?f?J) literal 0 HcmV?d00001 diff --git a/collect_app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/collect_app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..61b76df67b86c231f2b317e42aee467d7ca42204 GIT binary patch literal 2766 zcmV;<3NiIkNk&G-3IG6CMM6+kP&iDv3IG5vzrZgLRfpQPZ5ZqS$DN)rA|`+iWkWX5 zOEWap`v2eE$^Luv-h1!8_g>R`?}~eWzvub=^ZU|O7RUnBA!2DV@j;DAM&HjItrTkWL_ngd9Qilr2T~c?C z+}+*X-QC^Y-CgGVvFLZ7^AF*N6Y6BNL!_q+tb|hyk*(#N5X;@&A$5p(D8i$uf^3SI zLMUQOm=KvF)=JoLi}>S^>10y_ZavHm>0xgXSQ%Tybifoq5ia4s0E+mfw*ay!ga&SO zrzBi@@D5G|JX64>hm&r;1#AfsaJFqb-W;)Q+o^5a?cB|rZ1)dnx2JZqZELdUIC&<3 zw%MjY2$VnzjOmAyPL^@PNs`25*vSe`n21R5g-Ljs{xOFW5rrLe0+}Ha@nS&iiWgxK z4q-R>slxi!A2rGlWirT;L?T+O3)o5n%wBJ~%6UXdR7oOVr`B4y442O(fj;*>``o3q z-l&Txi6}!%k)XfMEwmnAc6x`WyL(N0)^u^LSPajw#T9+IUz$ z6_C)U{-1g5tePks_0c8=`YtJ1ALh6KqUsKbq2^iB$$W2m9~rr86PV|-SL0a&VK9u0 z{b-&*!zL&DL>ILU757o!I1J_it#Ht;kKzg{2?J7kyoT+ z+-Ite|G*HL_yeo^W`Jr#;@W`!5Z$8RT%Py`!7=Q_JVava3I6V=D*=r1j_XR@NO*Vl z+HWC)A|qLKK;EQ4XYx^03jvW9uiHUiQlvGvm_eZ-BUQD9exy)qaU;|~pp|!>C8Stq z{4r?FgJ9^9aigxJU~lm_>P7%y{2Pw6k4e#~vBt~ttLQqU?AHEENqQ?94PlRbT!?$K zfgf990E)0p9+zEN+a+#q=`lN3w4)J+U#L~??1jq?{tj4)lIsZ8FL|uFg{%dQtBrYL z8R=>(2hEXOmlOKraHDS3LsLbgzg*G`mT-9Srx9mLZh57Y*iDNy6i_XyhdP@r`0F*% zrKW}G7i_k`?>ZDT#yw!8q;OkmW8bK+Y|rLG-B*Y*;!HYXFSbDKT9es;r8u80EmlWi zBb{S5U-`a8W>r0ln3v2Ir=SwExzV>Fdln2UE#my=fWDbJdyh3RG}ntW`i8rZcP5$( zFpC>~{Vp-sNl(@)d-j&DU76`ha-_A%3Nm%{9Uu2XG_1XTLutz&xDf!R(0!U)QPK+D zigJt|IwilHtk-}x;zF%j+Df~1!k-cL$=M+5J=MI$;|!|1ySN@Sz^i6pbQAM0TflX; zD|sW%s=fuSHc#P9oQ)$dB(T~WiPKX!4c>AHtBcMh&RM-D8p7)F=ZMo>()1BnIr)4B zPhl<6fK^nBI*Ie&#}QUFL!+BGBOT*amJBBzLk3?3>EilO|rc7iaPEY>)KC+Jg z!pY&*E|`=yy;dgP%{!1)eX(;cOg79i+}d@>T-NelC7@uKWoED?mmqV~d;-fohZv~! zIAk8Y<2=|4!37gT9o-;$VxaRf)7Tg4r`Qo>J_ypR3=(dg_8{xy!P+H(28SK#V-0X? z8?tVTa*hc&c=cwkueKuVqHr?Hsw zO6kkUdg_-1BD9=lu48Rhrffjgty2j|s4n;9PgWw`33~=-pIuBW6T*JfL z$NwNhkN?4K`Nbr{I+;zpv;UG^PoQSaC47W^`nc?{NmrI>9!js{G-ahkn=i``-*;y& zN;+U~)j*7~Z0wQW(ukSMIumfF>%9X|Bd|%ZJr`)wq_-@=N z!zw2}cXR<;j~U;!L&VS;-tNV`$%mGc-)H@4EA+co2?zk$1l9)5dZ$vdDH!f`VahuY z*^T2A0zfxIjV(VK&6kqpV6=tUPacXEX|b^jHv+w>M?T`wIict3ShI8t>@`9HHprJa zx!UKjWW&l^8OFW^RjMJf#|QwGXqpR>fr@VV{e8)>{2Rmc&_hk6NnYYC6mC6?`X!(l zZ)6GB*z>+66P7am$he%Zle!zI@1~Ck|{t z6e_1sd}OB@QGyP*;a>Jfics=dfCEY_Qj4}> zpRbDf74t6|)=EJ;MeUWEeY<5R9`;}Li@&=2`AF>T5@#_EV$Ti5BA_mAYRI=Czhb@@ zSC_zxpR8JS`~tD^#5`i@lM+>LwE;AjY$~QB!t*U|0 zY<19vlyN9DyA2^TsT?uOY-b%V$eeMT)mf)o)n;c53o>Izt#ZsvdI4tl!V55kGE+2F zU1n#hGTTaT-e%wQmclk;*k&tRYNJBCnC)97Z)B3G^#aTgsg18-K=Cc-+oq7+hFl#Q z2H!!O+5W$Pj?C;fv%QSU#u?MCQf7`?;SCKpx}(I*w#;652{JQxx>>&fZDz*KR&7){ zBKgj|0E5%mw$*C&Uqvj(GH9-YR9rr~jU-8t!+FeX1gi8BYcGMet>cZ>#7=EDwrXQ+ z+qP}nwr#VvZDUe9&j060aN9_dBt-@?)8UMoJ*&EAUO+Y4|95iJe;+yLoO8}O=bW>0 z)_(u{`E3Il6(H3hWSVKD;u3NL8Z{tc%_7GxVguK~AyIK>cFdVsE+88%^ZXpB(G}n# z+<>SGSaYqA&@qo|I2GY0Z0vCXD&P*CRD;o0p&DFB#>aQeHOHK&+i(MFz%=3I5>p4N z!D-8IDnfOb9={Pks#08pX`~T(=F5c~2>@V3`~RzL+qP}n#$=bn1^~!5pW3$V*|yF8 z! zn8JDXJptALm~?6*MIEpHPk=Ji4=^7?O^Gn^?C<|iuZ|j|^tgzWk#d*=$i-PvWl;>_ zaDdrTMrxyndfkBE=;|<-6@?Zm)DU{zG!W4hz$FP_gz(5lHT5D5!dp*z8-_`sy2@cb zpaDXnK2&N0;n^A}E!r3c;0bsQ;bB-cWy7U{P%-Pl)A)Frhs z&kwjsbc(u%0Jol>u}P_D3nU0ogr`zn+*37zQ5W@WL974_!6O%|X%Vh8$Ysc%m8gqi z=>eR_Q)wX@t8>8*ER#T~90D6IpCCL8eNwFshsJ7r?g2X{fkNpIYdClqg_>63I*c)Z zynf(PWk?g4>L>yAydE?Q(~+S+4gKLyja#VH#sm@r=!E++s_7S8Owgas0T@Yz>qK@8etfR$=|@oC24d;fAsw^!zt)xf|J8|Y|?gAL*Exuoe2WY zK%%}ElQbSfqv?&2&IEzxFem`+BrhqLo*)^c1Uj>6P!S!NKa2 z)&`ha9E5G^A?WCqKxcv=?>B&2GP}(YfEf%@dsSj{-H_nS&3Lww{H?@)wPh{&d00*% zky(7H)A$xT1fF3328 zI0F@A-R(P3A}Teom%mtm9MzAwV%LHi0Vsw6`rxvYkqV*%hs6*o)TCf3oCkLDHw%)p z_FlSmKz+E3LjXOo#+e|190or?KRk?@6naTCI@cFw%npiYW-m8Lh`1o)ff?wWP|}Bu z=?MmxJhDvP7#&9_V8w%)|hfXP}o{Mw_FoP`9BQjKwtcC10QGG;7Gg{0*1pgk~Dpvjr6Mt{IwgDefHlYZ)J>> zyp93Pgzy+@GT1!H8X^7oE?KF{NkQm~6yKrK+v-YMS zSA|BW5_C#`Kou4mLVox6IQ<}VNGEHqIjf)0Jkbgm2~yLW_p%*m#=8j|=Qo-u)f)a!xL&0y{H0~^wL7?zMV%MOIHk$fr6F5g-bs+?g$ z23tRbauZf$lc7!s=TeY3WRO*tbyt}wE}FtJi-G*zYr`e*Q%aahrh(rbH3uv1~hDk$o&+!KGjkH+?2j>)ysz*DZ&>SMEt_1BAV^&l?LFVGI#W&D~w1_XQ{KT$Vbkkph@2SoiSCGOzPNkAYQJp9gmpyVf*FX%M=EBQ9efKpB>KiJ!fsrNHTi<`mjhIHFcO zSKHjweW{fAkr9F9&p#MWH)hgs>yN3qsKe>)GIt!;-jvoqV?ZNT?Y~3P5ooe6C9HVc zhJ{w>g|2Ajk7;?aY~`P@#H4I%_oH_lziZT+Md~Fi9Nm6t3TUDex`Uq9qpCS`ZE#@T z{NunuVQ-+#GRs~H0s+pW>jq&PTA>lf{AskERm2=Fclx<4s&LO-aWRC*DZdR54s6EPx*S|C~n<2+<(g6@a{^Y^Y_lyCs(bG6Jp>)!F17kC8fP`rdHdSJ}zXIdIx z$F>t8C7#1f)Gps7qvNt1u=f06`3qh2pm^oIMBB!u4=-f$;9)D1hQP3&5^gWuEMW$a z4LP8+;QVpg-mbgJdLax8RH0;gIOxs{m&lFbQk`-NVwyoC7Mga5@-UqgSoSCt^wT4& z6KaP9GB_k*9H;JW3 z^qXdkWW)zo?oPPg=Lm}q=f4oR|Iq#W8HwK&1*s=eX?M4vPaF}Yqy zXa>)lUDLHTLM4xn&nDmo&_FR_a(#7aIs5_9{c;Wm^zo_iaZ6{kt_&qUOMoa@A)fw} zQH+L%s3!Y*7BvA6U56(Cs7lSE;Y$_{36Iew2=>E}MKL-t@;9|Oe;?5!PRYjoi{Y@< zfc7XxaFxsn0xntvyCL{Xwdby!QTm`E;CRKslmyjhZJgnV+5NXkT8egb9Gu=cLCYSP z=g>th3_rp@pX5y6=Gy;_&!3UqyGg31hs1(>3l;*v(kHMpj>j$srVN+m zKtr&u$XsYNQH+=9eB6P*A1XgFMBU!e*@0OICfGBUt+OuuFYs9`R0OLX6TxACdhacd zRI$E*AG~e*c8I3-uF|p)T*fXLTpts27Y+><1k3^;BQRG9*PnWrNxAnPhx#32+>Pgx zl$CCrg|~(vvZ~U$L}a3QK|#Pc8Jt$LhKRsGa2}il$N{0=dz*o;9zKMK6;)?lPf&R< zeq}L;Rr_y^90U)|%pn;VJaku&;MgzsDEWPduRni+3w};M8Yfv80|KRaS90|_eC+dr ziQv!`+u<=x)Z;dv_Kk5rX}m&pxaM9K1qVxU@4${Zr96%u7pM0hvYThZnHXIZeD8Op zS9q$A33gRC!-agcacCV2!v(v>)C3E&=Lf^<#OIb`*MP*II+#hx_m;oh^)|yrKj0cYVPBZun`R*09=> z0ewejc5<^oRQKK@X$YRVGlU|;#}IkD`W`uj7HIT+=c(vSpT%+L;zmt!M}NDKXfOPO z3&y&n9COl)F5!sF*d55GSX;6HY|xYV*yjf$W#!1KQ3o@HBW%s~X;}gxDVUS^#)ebD z7Qww=K0udHM9e-{8U7V~QA4kj0GWGU*de%7X1md75{8&y&%m}h6*Uj+-7+E!y18S8 z;Phd`(g`z#A+D^B=v@sH9CN{1hdPN0(}9+P;MFgGAO}PUB32ColqVg*H|3^z6{3Q5 z*4QUjWRLh5CgF#vxF=v+oJ%}?tJg`0Eb?}v)=%UxbihpEhbyTHJu78g)a#Jx_R(vo(gt}sXdaOMACHNR=SXR2uRQ$e7&5I zy;C56&SfY-5F}0+`(SQ9&7xW1p0`8R42p%Ur}u{yya4qP&KjSBtKmvs^3GwYI4dZZ^xc#J-_T2ApKjKUi{5xGqaGa$!1l-xGq+iXNY^Q zJNT?L@O~$cS87$>?cYVtRJ8ObSbaJS;X2(;{(;)B4g zz{L+g#-SmtbnR%)cT4K7GUl9PbPa5SJ2^AKH#;BOIYedAv~bOvI?}Gme^_#!UM%lu z3KtkLBbI{FluZp~-ZXI&Vd^29?2^!j%-KAP`oxrlIpwWM&0u3IU@+TiR>HI%*5*89 zs06RBPVR9`4d0kndGuVFc&~rEBYjI8XE34 zi$=Q_a}e90Jq&ASb?S^^E630g&WeY>x%z3t*;8q*-bAK}oX5ui?@Dy^$mD1bXxoo% zi~D=JA$=~+11||j4m=!!q!P)`^)uvn4u=G1LKk-)nUUiOv3HcFC z$#h_)E{WJKXr73qw}8`0mFC&v*pfGnH=)Xe75Kjx#-#pNiH3KEoG_qL)C}S7X>+VP z*9Mv73}f-qbx=P=@j}Jk?8g}UuKU?>tj6yZc8PcS2rUef{)Fh|tC%M$1jWPND z1`%vethV$LWLX^oTjI(yKIb?%8d=g92oAS9S$GKIay+jUh{Z(KTcn1kxF5c-A~G?2 z$|Nxy!_)<^eLh5g?d0)Rj)AK|0=h+x=ZJa#)`)8q2zqGpKFaMHho<^f&0h{TP0V%= zG)XFgjCaR$V>^;4Y15;o4R=qoCa_)dCM5AqzRu9_61T4Nw(qroG{1Z-sn%?VsF^Gb z{9g~%>o-TqED7Dyq*3Ww{T@Rdo4MDF)?C!8qpr4oT5FU@ zsouOa^$TP4=ga{9IH7otyU=4&BBqbv8CT4ts17brDuuYn;SgV!oTaJ0N~^y-*h33dO# z&(Ias>6N2n<;;&hJBveP_2OtGH?Zb95WJ^{D0F?VxvZy9^#@I5mc7=?tlj2^h#AyXTVNLnIR!X z(?v2;RIG$B;r;OjdSEgrn&d^7z(lkfs{L01w!=GL^l8TPQii6WhdzE9h@vNhY@3|v z5ZyWdjEOF)NmbAm`pt4NIr?PTnAE+ob9Bt7CD|rN?ELp=IrC zN`9Ibvgp%^Qed!L2ZHD1;7%9mWRNa|ku8vdx=eIXAGTv)XQVHbF`qjfWM5n0$kp0; zNr@pUiHLihqRAlNHa9AxZS*iM%BzagYZL93xZ^8&ow$e+5%#Lo7*bceU&*eMP$V!MTXZsMt0#yQ$`q;JO%uBz~jp3-aLz? z>`KjYfks!gO`cDIz~r!-9;q;U47Zn;)ESZav3kpJclNpRHL)+dRV)QBqN;hs7HQuc z*C&I2a@hZy6H9U>IvF1+ykEOjlU<15or8*tXTnbTP!Uc2nk-8xMa;6tzvgz(Y>&^2 zWs>}J?<^<2Fk53KxFm&II0~$Vkxvz9luc}OgkfK z)vWL(XVfr9IMNYl_&q4*(uz=VxM?7n60&69y4kc#GOJ?l4QPNa#MD@2y?GVPO_8;B zw)m1i&MZUr|-mgz5{-lSMG`=gNJnW!X;n(gxjK0e6(evL>;>6cfv; zPx42t)3XD44`R8WbytzOhstKl2cPxH_OL9{REpat?izPF88{95SWk;a(8yi)q zAQpt8g7s>31X7x+t#EfSOh#7|*NC$ersPT@(Vp0(vdBB6ER2t<5jJ(?a^ExvSsy!g z%FotN|Ja*9tWm-8N~HExzy>#QMqj%Vey*FH5}&+oSdF;&LM881FqP^HF9N|jMtaFO zmbjR(X``47GP!TzjN1gqc0t_N{#b9oy=Orda~W|6y_i357K-=Z=!{xNy>sH4;KV|i z;FBkh78Vm{8Aq8uhE?aqtY|QTEtG`?=A0=Z$(Rx{xzD!n$)|)Y;~+Wag2Lb{Ueaoj zqK_D9xSO96(z-lh=0yI@7P+=ZSTtP0f<;52D2DL(4|_+w z0q<3r!3xxzbPHTxHLGVvln+(v?ykmgBSq*_@K^L&9iwZP?g~`h z)-^upidlSqwSDsBjL~*?^EqbAIF3+JKHiFM3%gsZLG6jC5Oa`>(URqIpRe`|2)m}$ zvbGdQx5SO@@$RH8kX4S-M{S*^c4=*&_DXbBp}Q*GUsaLz_G$fymZ>RE$%xytkN9xBfvsJvA=R zGLBvui}531gRf{Q7*IO`ktd`$3M0@`PE3ub`HcEAKs6Io8B;@Xi*kav#St@}@-6Cr z#?#b@$w6xog^m=S5{dc^xC&kEJOP2K$qh|$L?sr)=b4AgNsU1eeEQSuZg*i}b + + #FFFFFF + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index c75d1812be1..2afa9221073 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,8 +3,8 @@ android.useAndroidX=true android.enableJetifier=true #https://issuetracker.google.com/issues/283715193 android.jetifier.ignorelist = jackson-core -org.gradle.jvmargs=-Xmx1536m +org.gradle.jvmargs=-Xmx4096m org.gradle.parallel=true -test.heap.max=1g +test.heap.max=4g android.nonTransitiveRClass=true android.injected.androidTest.leaveApksInstalledAfterRun=true diff --git a/node_modules/.bin/replace b/node_modules/.bin/replace new file mode 120000 index 00000000000..b83ac83e67e --- /dev/null +++ b/node_modules/.bin/replace @@ -0,0 +1 @@ +../replace/bin/replace.js \ No newline at end of file diff --git a/node_modules/.bin/search b/node_modules/.bin/search new file mode 120000 index 00000000000..babeddef048 --- /dev/null +++ b/node_modules/.bin/search @@ -0,0 +1 @@ +../replace/bin/search.js \ No newline at end of file diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 00000000000..46d45acbf37 --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,365 @@ +{ + "name": "collect", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/replace": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/replace/-/replace-1.2.2.tgz", + "integrity": "sha512-C4EDifm22XZM2b2JOYe6Mhn+lBsLBAvLbK8drfUQLTfD1KYl/n3VaW/CDju0Ny4w3xTtegBpg8YNSpFJPUDSjA==", + "dependencies": { + "chalk": "2.4.2", + "minimatch": "3.0.5", + "yargs": "^15.3.1" + }, + "bin": { + "replace": "bin/replace.js", + "search": "bin/search.js" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/node_modules/ansi-regex/index.d.ts b/node_modules/ansi-regex/index.d.ts new file mode 100644 index 00000000000..2dbf6af2b6f --- /dev/null +++ b/node_modules/ansi-regex/index.d.ts @@ -0,0 +1,37 @@ +declare namespace ansiRegex { + interface Options { + /** + Match only the first ANSI escape. + + @default false + */ + onlyFirst: boolean; + } +} + +/** +Regular expression for matching ANSI escape codes. + +@example +``` +import ansiRegex = require('ansi-regex'); + +ansiRegex().test('\u001B[4mcake\u001B[0m'); +//=> true + +ansiRegex().test('cake'); +//=> false + +'\u001B[4mcake\u001B[0m'.match(ansiRegex()); +//=> ['\u001B[4m', '\u001B[0m'] + +'\u001B[4mcake\u001B[0m'.match(ansiRegex({onlyFirst: true})); +//=> ['\u001B[4m'] + +'\u001B]8;;https://github.com\u0007click\u001B]8;;\u0007'.match(ansiRegex()); +//=> ['\u001B]8;;https://github.com\u0007', '\u001B]8;;\u0007'] +``` +*/ +declare function ansiRegex(options?: ansiRegex.Options): RegExp; + +export = ansiRegex; diff --git a/node_modules/ansi-regex/index.js b/node_modules/ansi-regex/index.js new file mode 100644 index 00000000000..616ff837d3f --- /dev/null +++ b/node_modules/ansi-regex/index.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = ({onlyFirst = false} = {}) => { + const pattern = [ + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))' + ].join('|'); + + return new RegExp(pattern, onlyFirst ? undefined : 'g'); +}; diff --git a/node_modules/ansi-regex/license b/node_modules/ansi-regex/license new file mode 100644 index 00000000000..e7af2f77107 --- /dev/null +++ b/node_modules/ansi-regex/license @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/ansi-regex/package.json b/node_modules/ansi-regex/package.json new file mode 100644 index 00000000000..017f53116a9 --- /dev/null +++ b/node_modules/ansi-regex/package.json @@ -0,0 +1,55 @@ +{ + "name": "ansi-regex", + "version": "5.0.1", + "description": "Regular expression for matching ANSI escape codes", + "license": "MIT", + "repository": "chalk/ansi-regex", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "sindresorhus.com" + }, + "engines": { + "node": ">=8" + }, + "scripts": { + "test": "xo && ava && tsd", + "view-supported": "node fixtures/view-codes.js" + }, + "files": [ + "index.js", + "index.d.ts" + ], + "keywords": [ + "ansi", + "styles", + "color", + "colour", + "colors", + "terminal", + "console", + "cli", + "string", + "tty", + "escape", + "formatting", + "rgb", + "256", + "shell", + "xterm", + "command-line", + "text", + "regex", + "regexp", + "re", + "match", + "test", + "find", + "pattern" + ], + "devDependencies": { + "ava": "^2.4.0", + "tsd": "^0.9.0", + "xo": "^0.25.3" + } +} diff --git a/node_modules/ansi-regex/readme.md b/node_modules/ansi-regex/readme.md new file mode 100644 index 00000000000..4d848bc36f6 --- /dev/null +++ b/node_modules/ansi-regex/readme.md @@ -0,0 +1,78 @@ +# ansi-regex + +> Regular expression for matching [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) + + +## Install + +``` +$ npm install ansi-regex +``` + + +## Usage + +```js +const ansiRegex = require('ansi-regex'); + +ansiRegex().test('\u001B[4mcake\u001B[0m'); +//=> true + +ansiRegex().test('cake'); +//=> false + +'\u001B[4mcake\u001B[0m'.match(ansiRegex()); +//=> ['\u001B[4m', '\u001B[0m'] + +'\u001B[4mcake\u001B[0m'.match(ansiRegex({onlyFirst: true})); +//=> ['\u001B[4m'] + +'\u001B]8;;https://github.com\u0007click\u001B]8;;\u0007'.match(ansiRegex()); +//=> ['\u001B]8;;https://github.com\u0007', '\u001B]8;;\u0007'] +``` + + +## API + +### ansiRegex(options?) + +Returns a regex for matching ANSI escape codes. + +#### options + +Type: `object` + +##### onlyFirst + +Type: `boolean`
+Default: `false` *(Matches any ANSI escape codes in a string)* + +Match only the first ANSI escape. + + +## FAQ + +### Why do you test for codes not in the ECMA 48 standard? + +Some of the codes we run as a test are codes that we acquired finding various lists of non-standard or manufacturer specific codes. We test for both standard and non-standard codes, as most of them follow the same or similar format and can be safely matched in strings without the risk of removing actual string content. There are a few non-standard control codes that do not follow the traditional format (i.e. they end in numbers) thus forcing us to exclude them from the test because we cannot reliably match them. + +On the historical side, those ECMA standards were established in the early 90's whereas the VT100, for example, was designed in the mid/late 70's. At that point in time, control codes were still pretty ungoverned and engineers used them for a multitude of things, namely to activate hardware ports that may have been proprietary. Somewhere else you see a similar 'anarchy' of codes is in the x86 architecture for processors; there are a ton of "interrupts" that can mean different things on certain brands of processors, most of which have been phased out. + + +## Maintainers + +- [Sindre Sorhus](https://github.com/sindresorhus) +- [Josh Junon](https://github.com/qix-) + + +--- + +

+ + Get professional support for this package with a Tidelift subscription + +
+ + Tidelift helps make open source sustainable for maintainers while giving companies
assurances about security, maintenance, and licensing for their dependencies. +
+
diff --git a/node_modules/ansi-styles/index.js b/node_modules/ansi-styles/index.js new file mode 100644 index 00000000000..90a871c4d78 --- /dev/null +++ b/node_modules/ansi-styles/index.js @@ -0,0 +1,165 @@ +'use strict'; +const colorConvert = require('color-convert'); + +const wrapAnsi16 = (fn, offset) => function () { + const code = fn.apply(colorConvert, arguments); + return `\u001B[${code + offset}m`; +}; + +const wrapAnsi256 = (fn, offset) => function () { + const code = fn.apply(colorConvert, arguments); + return `\u001B[${38 + offset};5;${code}m`; +}; + +const wrapAnsi16m = (fn, offset) => function () { + const rgb = fn.apply(colorConvert, arguments); + return `\u001B[${38 + offset};2;${rgb[0]};${rgb[1]};${rgb[2]}m`; +}; + +function assembleStyles() { + const codes = new Map(); + const styles = { + modifier: { + reset: [0, 0], + // 21 isn't widely supported and 22 does the same thing + bold: [1, 22], + dim: [2, 22], + italic: [3, 23], + underline: [4, 24], + inverse: [7, 27], + hidden: [8, 28], + strikethrough: [9, 29] + }, + color: { + black: [30, 39], + red: [31, 39], + green: [32, 39], + yellow: [33, 39], + blue: [34, 39], + magenta: [35, 39], + cyan: [36, 39], + white: [37, 39], + gray: [90, 39], + + // Bright color + redBright: [91, 39], + greenBright: [92, 39], + yellowBright: [93, 39], + blueBright: [94, 39], + magentaBright: [95, 39], + cyanBright: [96, 39], + whiteBright: [97, 39] + }, + bgColor: { + bgBlack: [40, 49], + bgRed: [41, 49], + bgGreen: [42, 49], + bgYellow: [43, 49], + bgBlue: [44, 49], + bgMagenta: [45, 49], + bgCyan: [46, 49], + bgWhite: [47, 49], + + // Bright color + bgBlackBright: [100, 49], + bgRedBright: [101, 49], + bgGreenBright: [102, 49], + bgYellowBright: [103, 49], + bgBlueBright: [104, 49], + bgMagentaBright: [105, 49], + bgCyanBright: [106, 49], + bgWhiteBright: [107, 49] + } + }; + + // Fix humans + styles.color.grey = styles.color.gray; + + for (const groupName of Object.keys(styles)) { + const group = styles[groupName]; + + for (const styleName of Object.keys(group)) { + const style = group[styleName]; + + styles[styleName] = { + open: `\u001B[${style[0]}m`, + close: `\u001B[${style[1]}m` + }; + + group[styleName] = styles[styleName]; + + codes.set(style[0], style[1]); + } + + Object.defineProperty(styles, groupName, { + value: group, + enumerable: false + }); + + Object.defineProperty(styles, 'codes', { + value: codes, + enumerable: false + }); + } + + const ansi2ansi = n => n; + const rgb2rgb = (r, g, b) => [r, g, b]; + + styles.color.close = '\u001B[39m'; + styles.bgColor.close = '\u001B[49m'; + + styles.color.ansi = { + ansi: wrapAnsi16(ansi2ansi, 0) + }; + styles.color.ansi256 = { + ansi256: wrapAnsi256(ansi2ansi, 0) + }; + styles.color.ansi16m = { + rgb: wrapAnsi16m(rgb2rgb, 0) + }; + + styles.bgColor.ansi = { + ansi: wrapAnsi16(ansi2ansi, 10) + }; + styles.bgColor.ansi256 = { + ansi256: wrapAnsi256(ansi2ansi, 10) + }; + styles.bgColor.ansi16m = { + rgb: wrapAnsi16m(rgb2rgb, 10) + }; + + for (let key of Object.keys(colorConvert)) { + if (typeof colorConvert[key] !== 'object') { + continue; + } + + const suite = colorConvert[key]; + + if (key === 'ansi16') { + key = 'ansi'; + } + + if ('ansi16' in suite) { + styles.color.ansi[key] = wrapAnsi16(suite.ansi16, 0); + styles.bgColor.ansi[key] = wrapAnsi16(suite.ansi16, 10); + } + + if ('ansi256' in suite) { + styles.color.ansi256[key] = wrapAnsi256(suite.ansi256, 0); + styles.bgColor.ansi256[key] = wrapAnsi256(suite.ansi256, 10); + } + + if ('rgb' in suite) { + styles.color.ansi16m[key] = wrapAnsi16m(suite.rgb, 0); + styles.bgColor.ansi16m[key] = wrapAnsi16m(suite.rgb, 10); + } + } + + return styles; +} + +// Make the export immutable +Object.defineProperty(module, 'exports', { + enumerable: true, + get: assembleStyles +}); diff --git a/node_modules/ansi-styles/license b/node_modules/ansi-styles/license new file mode 100644 index 00000000000..e7af2f77107 --- /dev/null +++ b/node_modules/ansi-styles/license @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/ansi-styles/package.json b/node_modules/ansi-styles/package.json new file mode 100644 index 00000000000..65edb48c399 --- /dev/null +++ b/node_modules/ansi-styles/package.json @@ -0,0 +1,56 @@ +{ + "name": "ansi-styles", + "version": "3.2.1", + "description": "ANSI escape codes for styling strings in the terminal", + "license": "MIT", + "repository": "chalk/ansi-styles", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "sindresorhus.com" + }, + "engines": { + "node": ">=4" + }, + "scripts": { + "test": "xo && ava", + "screenshot": "svg-term --command='node screenshot' --out=screenshot.svg --padding=3 --width=55 --height=3 --at=1000 --no-cursor" + }, + "files": [ + "index.js" + ], + "keywords": [ + "ansi", + "styles", + "color", + "colour", + "colors", + "terminal", + "console", + "cli", + "string", + "tty", + "escape", + "formatting", + "rgb", + "256", + "shell", + "xterm", + "log", + "logging", + "command-line", + "text" + ], + "dependencies": { + "color-convert": "^1.9.0" + }, + "devDependencies": { + "ava": "*", + "babel-polyfill": "^6.23.0", + "svg-term-cli": "^2.1.1", + "xo": "*" + }, + "ava": { + "require": "babel-polyfill" + } +} diff --git a/node_modules/ansi-styles/readme.md b/node_modules/ansi-styles/readme.md new file mode 100644 index 00000000000..3158e2df59c --- /dev/null +++ b/node_modules/ansi-styles/readme.md @@ -0,0 +1,147 @@ +# ansi-styles [![Build Status](https://travis-ci.org/chalk/ansi-styles.svg?branch=master)](https://travis-ci.org/chalk/ansi-styles) + +> [ANSI escape codes](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors_and_Styles) for styling strings in the terminal + +You probably want the higher-level [chalk](https://github.com/chalk/chalk) module for styling your strings. + + + + +## Install + +``` +$ npm install ansi-styles +``` + + +## Usage + +```js +const style = require('ansi-styles'); + +console.log(`${style.green.open}Hello world!${style.green.close}`); + + +// Color conversion between 16/256/truecolor +// NOTE: If conversion goes to 16 colors or 256 colors, the original color +// may be degraded to fit that color palette. This means terminals +// that do not support 16 million colors will best-match the +// original color. +console.log(style.bgColor.ansi.hsl(120, 80, 72) + 'Hello world!' + style.bgColor.close); +console.log(style.color.ansi256.rgb(199, 20, 250) + 'Hello world!' + style.color.close); +console.log(style.color.ansi16m.hex('#ABCDEF') + 'Hello world!' + style.color.close); +``` + +## API + +Each style has an `open` and `close` property. + + +## Styles + +### Modifiers + +- `reset` +- `bold` +- `dim` +- `italic` *(Not widely supported)* +- `underline` +- `inverse` +- `hidden` +- `strikethrough` *(Not widely supported)* + +### Colors + +- `black` +- `red` +- `green` +- `yellow` +- `blue` +- `magenta` +- `cyan` +- `white` +- `gray` ("bright black") +- `redBright` +- `greenBright` +- `yellowBright` +- `blueBright` +- `magentaBright` +- `cyanBright` +- `whiteBright` + +### Background colors + +- `bgBlack` +- `bgRed` +- `bgGreen` +- `bgYellow` +- `bgBlue` +- `bgMagenta` +- `bgCyan` +- `bgWhite` +- `bgBlackBright` +- `bgRedBright` +- `bgGreenBright` +- `bgYellowBright` +- `bgBlueBright` +- `bgMagentaBright` +- `bgCyanBright` +- `bgWhiteBright` + + +## Advanced usage + +By default, you get a map of styles, but the styles are also available as groups. They are non-enumerable so they don't show up unless you access them explicitly. This makes it easier to expose only a subset in a higher-level module. + +- `style.modifier` +- `style.color` +- `style.bgColor` + +###### Example + +```js +console.log(style.color.green.open); +``` + +Raw escape codes (i.e. without the CSI escape prefix `\u001B[` and render mode postfix `m`) are available under `style.codes`, which returns a `Map` with the open codes as keys and close codes as values. + +###### Example + +```js +console.log(style.codes.get(36)); +//=> 39 +``` + + +## [256 / 16 million (TrueColor) support](https://gist.github.com/XVilka/8346728) + +`ansi-styles` uses the [`color-convert`](https://github.com/Qix-/color-convert) package to allow for converting between various colors and ANSI escapes, with support for 256 and 16 million colors. + +To use these, call the associated conversion function with the intended output, for example: + +```js +style.color.ansi.rgb(100, 200, 15); // RGB to 16 color ansi foreground code +style.bgColor.ansi.rgb(100, 200, 15); // RGB to 16 color ansi background code + +style.color.ansi256.hsl(120, 100, 60); // HSL to 256 color ansi foreground code +style.bgColor.ansi256.hsl(120, 100, 60); // HSL to 256 color ansi foreground code + +style.color.ansi16m.hex('#C0FFEE'); // Hex (RGB) to 16 million color foreground code +style.bgColor.ansi16m.hex('#C0FFEE'); // Hex (RGB) to 16 million color background code +``` + + +## Related + +- [ansi-escapes](https://github.com/sindresorhus/ansi-escapes) - ANSI escape codes for manipulating the terminal + + +## Maintainers + +- [Sindre Sorhus](https://github.com/sindresorhus) +- [Josh Junon](https://github.com/qix-) + + +## License + +MIT diff --git a/node_modules/balanced-match/.github/FUNDING.yml b/node_modules/balanced-match/.github/FUNDING.yml new file mode 100644 index 00000000000..cea8b16e9ed --- /dev/null +++ b/node_modules/balanced-match/.github/FUNDING.yml @@ -0,0 +1,2 @@ +tidelift: "npm/balanced-match" +patreon: juliangruber diff --git a/node_modules/balanced-match/LICENSE.md b/node_modules/balanced-match/LICENSE.md new file mode 100644 index 00000000000..2cdc8e4148c --- /dev/null +++ b/node_modules/balanced-match/LICENSE.md @@ -0,0 +1,21 @@ +(MIT) + +Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/balanced-match/README.md b/node_modules/balanced-match/README.md new file mode 100644 index 00000000000..d2a48b6b49f --- /dev/null +++ b/node_modules/balanced-match/README.md @@ -0,0 +1,97 @@ +# balanced-match + +Match balanced string pairs, like `{` and `}` or `` and ``. Supports regular expressions as well! + +[![build status](https://secure.travis-ci.org/juliangruber/balanced-match.svg)](http://travis-ci.org/juliangruber/balanced-match) +[![downloads](https://img.shields.io/npm/dm/balanced-match.svg)](https://www.npmjs.org/package/balanced-match) + +[![testling badge](https://ci.testling.com/juliangruber/balanced-match.png)](https://ci.testling.com/juliangruber/balanced-match) + +## Example + +Get the first matching pair of braces: + +```js +var balanced = require('balanced-match'); + +console.log(balanced('{', '}', 'pre{in{nested}}post')); +console.log(balanced('{', '}', 'pre{first}between{second}post')); +console.log(balanced(/\s+\{\s+/, /\s+\}\s+/, 'pre { in{nest} } post')); +``` + +The matches are: + +```bash +$ node example.js +{ start: 3, end: 14, pre: 'pre', body: 'in{nested}', post: 'post' } +{ start: 3, + end: 9, + pre: 'pre', + body: 'first', + post: 'between{second}post' } +{ start: 3, end: 17, pre: 'pre', body: 'in{nest}', post: 'post' } +``` + +## API + +### var m = balanced(a, b, str) + +For the first non-nested matching pair of `a` and `b` in `str`, return an +object with those keys: + +* **start** the index of the first match of `a` +* **end** the index of the matching `b` +* **pre** the preamble, `a` and `b` not included +* **body** the match, `a` and `b` not included +* **post** the postscript, `a` and `b` not included + +If there's no match, `undefined` will be returned. + +If the `str` contains more `a` than `b` / there are unmatched pairs, the first match that was closed will be used. For example, `{{a}` will match `['{', 'a', '']` and `{a}}` will match `['', 'a', '}']`. + +### var r = balanced.range(a, b, str) + +For the first non-nested matching pair of `a` and `b` in `str`, return an +array with indexes: `[ , ]`. + +If there's no match, `undefined` will be returned. + +If the `str` contains more `a` than `b` / there are unmatched pairs, the first match that was closed will be used. For example, `{{a}` will match `[ 1, 3 ]` and `{a}}` will match `[0, 2]`. + +## Installation + +With [npm](https://npmjs.org) do: + +```bash +npm install balanced-match +``` + +## Security contact information + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. + +## License + +(MIT) + +Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/balanced-match/index.js b/node_modules/balanced-match/index.js new file mode 100644 index 00000000000..c67a64608df --- /dev/null +++ b/node_modules/balanced-match/index.js @@ -0,0 +1,62 @@ +'use strict'; +module.exports = balanced; +function balanced(a, b, str) { + if (a instanceof RegExp) a = maybeMatch(a, str); + if (b instanceof RegExp) b = maybeMatch(b, str); + + var r = range(a, b, str); + + return r && { + start: r[0], + end: r[1], + pre: str.slice(0, r[0]), + body: str.slice(r[0] + a.length, r[1]), + post: str.slice(r[1] + b.length) + }; +} + +function maybeMatch(reg, str) { + var m = str.match(reg); + return m ? m[0] : null; +} + +balanced.range = range; +function range(a, b, str) { + var begs, beg, left, right, result; + var ai = str.indexOf(a); + var bi = str.indexOf(b, ai + 1); + var i = ai; + + if (ai >= 0 && bi > 0) { + if(a===b) { + return [ai, bi]; + } + begs = []; + left = str.length; + + while (i >= 0 && !result) { + if (i == ai) { + begs.push(i); + ai = str.indexOf(a, i + 1); + } else if (begs.length == 1) { + result = [ begs.pop(), bi ]; + } else { + beg = begs.pop(); + if (beg < left) { + left = beg; + right = bi; + } + + bi = str.indexOf(b, i + 1); + } + + i = ai < bi && ai >= 0 ? ai : bi; + } + + if (begs.length) { + result = [ left, right ]; + } + } + + return result; +} diff --git a/node_modules/balanced-match/package.json b/node_modules/balanced-match/package.json new file mode 100644 index 00000000000..ce6073e0403 --- /dev/null +++ b/node_modules/balanced-match/package.json @@ -0,0 +1,48 @@ +{ + "name": "balanced-match", + "description": "Match balanced character pairs, like \"{\" and \"}\"", + "version": "1.0.2", + "repository": { + "type": "git", + "url": "git://github.com/juliangruber/balanced-match.git" + }, + "homepage": "https://github.com/juliangruber/balanced-match", + "main": "index.js", + "scripts": { + "test": "tape test/test.js", + "bench": "matcha test/bench.js" + }, + "devDependencies": { + "matcha": "^0.7.0", + "tape": "^4.6.0" + }, + "keywords": [ + "match", + "regexp", + "test", + "balanced", + "parse" + ], + "author": { + "name": "Julian Gruber", + "email": "mail@juliangruber.com", + "url": "http://juliangruber.com" + }, + "license": "MIT", + "testling": { + "files": "test/*.js", + "browsers": [ + "ie/8..latest", + "firefox/20..latest", + "firefox/nightly", + "chrome/25..latest", + "chrome/canary", + "opera/12..latest", + "opera/next", + "safari/5.1..latest", + "ipad/6.0..latest", + "iphone/6.0..latest", + "android-browser/4.2..latest" + ] + } +} diff --git a/node_modules/brace-expansion/LICENSE b/node_modules/brace-expansion/LICENSE new file mode 100644 index 00000000000..de3226673c3 --- /dev/null +++ b/node_modules/brace-expansion/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013 Julian Gruber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/brace-expansion/README.md b/node_modules/brace-expansion/README.md new file mode 100644 index 00000000000..6b4e0e16409 --- /dev/null +++ b/node_modules/brace-expansion/README.md @@ -0,0 +1,129 @@ +# brace-expansion + +[Brace expansion](https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html), +as known from sh/bash, in JavaScript. + +[![build status](https://secure.travis-ci.org/juliangruber/brace-expansion.svg)](http://travis-ci.org/juliangruber/brace-expansion) +[![downloads](https://img.shields.io/npm/dm/brace-expansion.svg)](https://www.npmjs.org/package/brace-expansion) +[![Greenkeeper badge](https://badges.greenkeeper.io/juliangruber/brace-expansion.svg)](https://greenkeeper.io/) + +[![testling badge](https://ci.testling.com/juliangruber/brace-expansion.png)](https://ci.testling.com/juliangruber/brace-expansion) + +## Example + +```js +var expand = require('brace-expansion'); + +expand('file-{a,b,c}.jpg') +// => ['file-a.jpg', 'file-b.jpg', 'file-c.jpg'] + +expand('-v{,,}') +// => ['-v', '-v', '-v'] + +expand('file{0..2}.jpg') +// => ['file0.jpg', 'file1.jpg', 'file2.jpg'] + +expand('file-{a..c}.jpg') +// => ['file-a.jpg', 'file-b.jpg', 'file-c.jpg'] + +expand('file{2..0}.jpg') +// => ['file2.jpg', 'file1.jpg', 'file0.jpg'] + +expand('file{0..4..2}.jpg') +// => ['file0.jpg', 'file2.jpg', 'file4.jpg'] + +expand('file-{a..e..2}.jpg') +// => ['file-a.jpg', 'file-c.jpg', 'file-e.jpg'] + +expand('file{00..10..5}.jpg') +// => ['file00.jpg', 'file05.jpg', 'file10.jpg'] + +expand('{{A..C},{a..c}}') +// => ['A', 'B', 'C', 'a', 'b', 'c'] + +expand('ppp{,config,oe{,conf}}') +// => ['ppp', 'pppconfig', 'pppoe', 'pppoeconf'] +``` + +## API + +```js +var expand = require('brace-expansion'); +``` + +### var expanded = expand(str) + +Return an array of all possible and valid expansions of `str`. If none are +found, `[str]` is returned. + +Valid expansions are: + +```js +/^(.*,)+(.+)?$/ +// {a,b,...} +``` + +A comma separated list of options, like `{a,b}` or `{a,{b,c}}` or `{,a,}`. + +```js +/^-?\d+\.\.-?\d+(\.\.-?\d+)?$/ +// {x..y[..incr]} +``` + +A numeric sequence from `x` to `y` inclusive, with optional increment. +If `x` or `y` start with a leading `0`, all the numbers will be padded +to have equal length. Negative numbers and backwards iteration work too. + +```js +/^-?\d+\.\.-?\d+(\.\.-?\d+)?$/ +// {x..y[..incr]} +``` + +An alphabetic sequence from `x` to `y` inclusive, with optional increment. +`x` and `y` must be exactly one character, and if given, `incr` must be a +number. + +For compatibility reasons, the string `${` is not eligible for brace expansion. + +## Installation + +With [npm](https://npmjs.org) do: + +```bash +npm install brace-expansion +``` + +## Contributors + +- [Julian Gruber](https://github.com/juliangruber) +- [Isaac Z. Schlueter](https://github.com/isaacs) + +## Sponsors + +This module is proudly supported by my [Sponsors](https://github.com/juliangruber/sponsors)! + +Do you want to support modules like this to improve their quality, stability and weigh in on new features? Then please consider donating to my [Patreon](https://www.patreon.com/juliangruber). Not sure how much of my modules you're using? Try [feross/thanks](https://github.com/feross/thanks)! + +## License + +(MIT) + +Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/brace-expansion/index.js b/node_modules/brace-expansion/index.js new file mode 100644 index 00000000000..0478be81eab --- /dev/null +++ b/node_modules/brace-expansion/index.js @@ -0,0 +1,201 @@ +var concatMap = require('concat-map'); +var balanced = require('balanced-match'); + +module.exports = expandTop; + +var escSlash = '\0SLASH'+Math.random()+'\0'; +var escOpen = '\0OPEN'+Math.random()+'\0'; +var escClose = '\0CLOSE'+Math.random()+'\0'; +var escComma = '\0COMMA'+Math.random()+'\0'; +var escPeriod = '\0PERIOD'+Math.random()+'\0'; + +function numeric(str) { + return parseInt(str, 10) == str + ? parseInt(str, 10) + : str.charCodeAt(0); +} + +function escapeBraces(str) { + return str.split('\\\\').join(escSlash) + .split('\\{').join(escOpen) + .split('\\}').join(escClose) + .split('\\,').join(escComma) + .split('\\.').join(escPeriod); +} + +function unescapeBraces(str) { + return str.split(escSlash).join('\\') + .split(escOpen).join('{') + .split(escClose).join('}') + .split(escComma).join(',') + .split(escPeriod).join('.'); +} + + +// Basically just str.split(","), but handling cases +// where we have nested braced sections, which should be +// treated as individual members, like {a,{b,c},d} +function parseCommaParts(str) { + if (!str) + return ['']; + + var parts = []; + var m = balanced('{', '}', str); + + if (!m) + return str.split(','); + + var pre = m.pre; + var body = m.body; + var post = m.post; + var p = pre.split(','); + + p[p.length-1] += '{' + body + '}'; + var postParts = parseCommaParts(post); + if (post.length) { + p[p.length-1] += postParts.shift(); + p.push.apply(p, postParts); + } + + parts.push.apply(parts, p); + + return parts; +} + +function expandTop(str) { + if (!str) + return []; + + // I don't know why Bash 4.3 does this, but it does. + // Anything starting with {} will have the first two bytes preserved + // but *only* at the top level, so {},a}b will not expand to anything, + // but a{},b}c will be expanded to [a}c,abc]. + // One could argue that this is a bug in Bash, but since the goal of + // this module is to match Bash's rules, we escape a leading {} + if (str.substr(0, 2) === '{}') { + str = '\\{\\}' + str.substr(2); + } + + return expand(escapeBraces(str), true).map(unescapeBraces); +} + +function identity(e) { + return e; +} + +function embrace(str) { + return '{' + str + '}'; +} +function isPadded(el) { + return /^-?0\d/.test(el); +} + +function lte(i, y) { + return i <= y; +} +function gte(i, y) { + return i >= y; +} + +function expand(str, isTop) { + var expansions = []; + + var m = balanced('{', '}', str); + if (!m || /\$$/.test(m.pre)) return [str]; + + var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); + var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); + var isSequence = isNumericSequence || isAlphaSequence; + var isOptions = m.body.indexOf(',') >= 0; + if (!isSequence && !isOptions) { + // {a},b} + if (m.post.match(/,.*\}/)) { + str = m.pre + '{' + m.body + escClose + m.post; + return expand(str); + } + return [str]; + } + + var n; + if (isSequence) { + n = m.body.split(/\.\./); + } else { + n = parseCommaParts(m.body); + if (n.length === 1) { + // x{{a,b}}y ==> x{a}y x{b}y + n = expand(n[0], false).map(embrace); + if (n.length === 1) { + var post = m.post.length + ? expand(m.post, false) + : ['']; + return post.map(function(p) { + return m.pre + n[0] + p; + }); + } + } + } + + // at this point, n is the parts, and we know it's not a comma set + // with a single entry. + + // no need to expand pre, since it is guaranteed to be free of brace-sets + var pre = m.pre; + var post = m.post.length + ? expand(m.post, false) + : ['']; + + var N; + + if (isSequence) { + var x = numeric(n[0]); + var y = numeric(n[1]); + var width = Math.max(n[0].length, n[1].length) + var incr = n.length == 3 + ? Math.abs(numeric(n[2])) + : 1; + var test = lte; + var reverse = y < x; + if (reverse) { + incr *= -1; + test = gte; + } + var pad = n.some(isPadded); + + N = []; + + for (var i = x; test(i, y); i += incr) { + var c; + if (isAlphaSequence) { + c = String.fromCharCode(i); + if (c === '\\') + c = ''; + } else { + c = String(i); + if (pad) { + var need = width - c.length; + if (need > 0) { + var z = new Array(need + 1).join('0'); + if (i < 0) + c = '-' + z + c.slice(1); + else + c = z + c; + } + } + } + N.push(c); + } + } else { + N = concatMap(n, function(el) { return expand(el, false) }); + } + + for (var j = 0; j < N.length; j++) { + for (var k = 0; k < post.length; k++) { + var expansion = pre + N[j] + post[k]; + if (!isTop || isSequence || expansion) + expansions.push(expansion); + } + } + + return expansions; +} + diff --git a/node_modules/brace-expansion/package.json b/node_modules/brace-expansion/package.json new file mode 100644 index 00000000000..a18faa8fd67 --- /dev/null +++ b/node_modules/brace-expansion/package.json @@ -0,0 +1,47 @@ +{ + "name": "brace-expansion", + "description": "Brace expansion as known from sh/bash", + "version": "1.1.11", + "repository": { + "type": "git", + "url": "git://github.com/juliangruber/brace-expansion.git" + }, + "homepage": "https://github.com/juliangruber/brace-expansion", + "main": "index.js", + "scripts": { + "test": "tape test/*.js", + "gentest": "bash test/generate.sh", + "bench": "matcha test/perf/bench.js" + }, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + }, + "devDependencies": { + "matcha": "^0.7.0", + "tape": "^4.6.0" + }, + "keywords": [], + "author": { + "name": "Julian Gruber", + "email": "mail@juliangruber.com", + "url": "http://juliangruber.com" + }, + "license": "MIT", + "testling": { + "files": "test/*.js", + "browsers": [ + "ie/8..latest", + "firefox/20..latest", + "firefox/nightly", + "chrome/25..latest", + "chrome/canary", + "opera/12..latest", + "opera/next", + "safari/5.1..latest", + "ipad/6.0..latest", + "iphone/6.0..latest", + "android-browser/4.2..latest" + ] + } +} diff --git a/node_modules/camelcase/index.d.ts b/node_modules/camelcase/index.d.ts new file mode 100644 index 00000000000..58f2069adc5 --- /dev/null +++ b/node_modules/camelcase/index.d.ts @@ -0,0 +1,63 @@ +declare namespace camelcase { + interface Options { + /** + Uppercase the first character: `foo-bar` → `FooBar`. + + @default false + */ + readonly pascalCase?: boolean; + } +} + +declare const camelcase: { + /** + Convert a dash/dot/underscore/space separated string to camelCase or PascalCase: `foo-bar` → `fooBar`. + + @param input - String to convert to camel case. + + @example + ``` + import camelCase = require('camelcase'); + + camelCase('foo-bar'); + //=> 'fooBar' + + camelCase('foo_bar'); + //=> 'fooBar' + + camelCase('Foo-Bar'); + //=> 'fooBar' + + camelCase('Foo-Bar', {pascalCase: true}); + //=> 'FooBar' + + camelCase('--foo.bar', {pascalCase: false}); + //=> 'fooBar' + + camelCase('foo bar'); + //=> 'fooBar' + + console.log(process.argv[3]); + //=> '--foo-bar' + camelCase(process.argv[3]); + //=> 'fooBar' + + camelCase(['foo', 'bar']); + //=> 'fooBar' + + camelCase(['__foo__', '--bar'], {pascalCase: true}); + //=> 'FooBar' + ``` + */ + (input: string | ReadonlyArray, options?: camelcase.Options): string; + + // TODO: Remove this for the next major release, refactor the whole definition to: + // declare function camelcase( + // input: string | ReadonlyArray, + // options?: camelcase.Options + // ): string; + // export = camelcase; + default: typeof camelcase; +}; + +export = camelcase; diff --git a/node_modules/camelcase/index.js b/node_modules/camelcase/index.js new file mode 100644 index 00000000000..579f99b47f7 --- /dev/null +++ b/node_modules/camelcase/index.js @@ -0,0 +1,76 @@ +'use strict'; + +const preserveCamelCase = string => { + let isLastCharLower = false; + let isLastCharUpper = false; + let isLastLastCharUpper = false; + + for (let i = 0; i < string.length; i++) { + const character = string[i]; + + if (isLastCharLower && /[a-zA-Z]/.test(character) && character.toUpperCase() === character) { + string = string.slice(0, i) + '-' + string.slice(i); + isLastCharLower = false; + isLastLastCharUpper = isLastCharUpper; + isLastCharUpper = true; + i++; + } else if (isLastCharUpper && isLastLastCharUpper && /[a-zA-Z]/.test(character) && character.toLowerCase() === character) { + string = string.slice(0, i - 1) + '-' + string.slice(i - 1); + isLastLastCharUpper = isLastCharUpper; + isLastCharUpper = false; + isLastCharLower = true; + } else { + isLastCharLower = character.toLowerCase() === character && character.toUpperCase() !== character; + isLastLastCharUpper = isLastCharUpper; + isLastCharUpper = character.toUpperCase() === character && character.toLowerCase() !== character; + } + } + + return string; +}; + +const camelCase = (input, options) => { + if (!(typeof input === 'string' || Array.isArray(input))) { + throw new TypeError('Expected the input to be `string | string[]`'); + } + + options = Object.assign({ + pascalCase: false + }, options); + + const postProcess = x => options.pascalCase ? x.charAt(0).toUpperCase() + x.slice(1) : x; + + if (Array.isArray(input)) { + input = input.map(x => x.trim()) + .filter(x => x.length) + .join('-'); + } else { + input = input.trim(); + } + + if (input.length === 0) { + return ''; + } + + if (input.length === 1) { + return options.pascalCase ? input.toUpperCase() : input.toLowerCase(); + } + + const hasUpperCase = input !== input.toLowerCase(); + + if (hasUpperCase) { + input = preserveCamelCase(input); + } + + input = input + .replace(/^[_.\- ]+/, '') + .toLowerCase() + .replace(/[_.\- ]+(\w|$)/g, (_, p1) => p1.toUpperCase()) + .replace(/\d+(\w|$)/g, m => m.toUpperCase()); + + return postProcess(input); +}; + +module.exports = camelCase; +// TODO: Remove this for the next major release +module.exports.default = camelCase; diff --git a/node_modules/camelcase/license b/node_modules/camelcase/license new file mode 100644 index 00000000000..e7af2f77107 --- /dev/null +++ b/node_modules/camelcase/license @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/camelcase/package.json b/node_modules/camelcase/package.json new file mode 100644 index 00000000000..fbdbaaa7565 --- /dev/null +++ b/node_modules/camelcase/package.json @@ -0,0 +1,43 @@ +{ + "name": "camelcase", + "version": "5.3.1", + "description": "Convert a dash/dot/underscore/space separated string to camelCase or PascalCase: `foo-bar` → `fooBar`", + "license": "MIT", + "repository": "sindresorhus/camelcase", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "sindresorhus.com" + }, + "engines": { + "node": ">=6" + }, + "scripts": { + "test": "xo && ava && tsd" + }, + "files": [ + "index.js", + "index.d.ts" + ], + "keywords": [ + "camelcase", + "camel-case", + "camel", + "case", + "dash", + "hyphen", + "dot", + "underscore", + "separator", + "string", + "text", + "convert", + "pascalcase", + "pascal-case" + ], + "devDependencies": { + "ava": "^1.4.1", + "tsd": "^0.7.1", + "xo": "^0.24.0" + } +} diff --git a/node_modules/camelcase/readme.md b/node_modules/camelcase/readme.md new file mode 100644 index 00000000000..fde27261b2a --- /dev/null +++ b/node_modules/camelcase/readme.md @@ -0,0 +1,99 @@ +# camelcase [![Build Status](https://travis-ci.org/sindresorhus/camelcase.svg?branch=master)](https://travis-ci.org/sindresorhus/camelcase) + +> Convert a dash/dot/underscore/space separated string to camelCase or PascalCase: `foo-bar` → `fooBar` + +--- + + + +--- + +## Install + +``` +$ npm install camelcase +``` + + +## Usage + +```js +const camelCase = require('camelcase'); + +camelCase('foo-bar'); +//=> 'fooBar' + +camelCase('foo_bar'); +//=> 'fooBar' + +camelCase('Foo-Bar'); +//=> 'fooBar' + +camelCase('Foo-Bar', {pascalCase: true}); +//=> 'FooBar' + +camelCase('--foo.bar', {pascalCase: false}); +//=> 'fooBar' + +camelCase('foo bar'); +//=> 'fooBar' + +console.log(process.argv[3]); +//=> '--foo-bar' +camelCase(process.argv[3]); +//=> 'fooBar' + +camelCase(['foo', 'bar']); +//=> 'fooBar' + +camelCase(['__foo__', '--bar'], {pascalCase: true}); +//=> 'FooBar' +``` + + +## API + +### camelCase(input, [options]) + +#### input + +Type: `string` `string[]` + +String to convert to camel case. + +#### options + +Type: `Object` + +##### pascalCase + +Type: `boolean`
+Default: `false` + +Uppercase the first character: `foo-bar` → `FooBar` + + +## Security + +To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. + + +## Related + +- [decamelize](https://github.com/sindresorhus/decamelize) - The inverse of this module +- [uppercamelcase](https://github.com/SamVerschueren/uppercamelcase) - Like this module, but to PascalCase instead of camelCase +- [titleize](https://github.com/sindresorhus/titleize) - Capitalize every word in string +- [humanize-string](https://github.com/sindresorhus/humanize-string) - Convert a camelized/dasherized/underscored string into a humanized one + + +## License + +MIT © [Sindre Sorhus](https://sindresorhus.com) diff --git a/node_modules/chalk/index.js b/node_modules/chalk/index.js new file mode 100644 index 00000000000..1cc5fa89a95 --- /dev/null +++ b/node_modules/chalk/index.js @@ -0,0 +1,228 @@ +'use strict'; +const escapeStringRegexp = require('escape-string-regexp'); +const ansiStyles = require('ansi-styles'); +const stdoutColor = require('supports-color').stdout; + +const template = require('./templates.js'); + +const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); + +// `supportsColor.level` → `ansiStyles.color[name]` mapping +const levelMapping = ['ansi', 'ansi', 'ansi256', 'ansi16m']; + +// `color-convert` models to exclude from the Chalk API due to conflicts and such +const skipModels = new Set(['gray']); + +const styles = Object.create(null); + +function applyOptions(obj, options) { + options = options || {}; + + // Detect level if not set manually + const scLevel = stdoutColor ? stdoutColor.level : 0; + obj.level = options.level === undefined ? scLevel : options.level; + obj.enabled = 'enabled' in options ? options.enabled : obj.level > 0; +} + +function Chalk(options) { + // We check for this.template here since calling `chalk.constructor()` + // by itself will have a `this` of a previously constructed chalk object + if (!this || !(this instanceof Chalk) || this.template) { + const chalk = {}; + applyOptions(chalk, options); + + chalk.template = function () { + const args = [].slice.call(arguments); + return chalkTag.apply(null, [chalk.template].concat(args)); + }; + + Object.setPrototypeOf(chalk, Chalk.prototype); + Object.setPrototypeOf(chalk.template, chalk); + + chalk.template.constructor = Chalk; + + return chalk.template; + } + + applyOptions(this, options); +} + +// Use bright blue on Windows as the normal blue color is illegible +if (isSimpleWindowsTerm) { + ansiStyles.blue.open = '\u001B[94m'; +} + +for (const key of Object.keys(ansiStyles)) { + ansiStyles[key].closeRe = new RegExp(escapeStringRegexp(ansiStyles[key].close), 'g'); + + styles[key] = { + get() { + const codes = ansiStyles[key]; + return build.call(this, this._styles ? this._styles.concat(codes) : [codes], this._empty, key); + } + }; +} + +styles.visible = { + get() { + return build.call(this, this._styles || [], true, 'visible'); + } +}; + +ansiStyles.color.closeRe = new RegExp(escapeStringRegexp(ansiStyles.color.close), 'g'); +for (const model of Object.keys(ansiStyles.color.ansi)) { + if (skipModels.has(model)) { + continue; + } + + styles[model] = { + get() { + const level = this.level; + return function () { + const open = ansiStyles.color[levelMapping[level]][model].apply(null, arguments); + const codes = { + open, + close: ansiStyles.color.close, + closeRe: ansiStyles.color.closeRe + }; + return build.call(this, this._styles ? this._styles.concat(codes) : [codes], this._empty, model); + }; + } + }; +} + +ansiStyles.bgColor.closeRe = new RegExp(escapeStringRegexp(ansiStyles.bgColor.close), 'g'); +for (const model of Object.keys(ansiStyles.bgColor.ansi)) { + if (skipModels.has(model)) { + continue; + } + + const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1); + styles[bgModel] = { + get() { + const level = this.level; + return function () { + const open = ansiStyles.bgColor[levelMapping[level]][model].apply(null, arguments); + const codes = { + open, + close: ansiStyles.bgColor.close, + closeRe: ansiStyles.bgColor.closeRe + }; + return build.call(this, this._styles ? this._styles.concat(codes) : [codes], this._empty, model); + }; + } + }; +} + +const proto = Object.defineProperties(() => {}, styles); + +function build(_styles, _empty, key) { + const builder = function () { + return applyStyle.apply(builder, arguments); + }; + + builder._styles = _styles; + builder._empty = _empty; + + const self = this; + + Object.defineProperty(builder, 'level', { + enumerable: true, + get() { + return self.level; + }, + set(level) { + self.level = level; + } + }); + + Object.defineProperty(builder, 'enabled', { + enumerable: true, + get() { + return self.enabled; + }, + set(enabled) { + self.enabled = enabled; + } + }); + + // See below for fix regarding invisible grey/dim combination on Windows + builder.hasGrey = this.hasGrey || key === 'gray' || key === 'grey'; + + // `__proto__` is used because we must return a function, but there is + // no way to create a function with a different prototype + builder.__proto__ = proto; // eslint-disable-line no-proto + + return builder; +} + +function applyStyle() { + // Support varags, but simply cast to string in case there's only one arg + const args = arguments; + const argsLen = args.length; + let str = String(arguments[0]); + + if (argsLen === 0) { + return ''; + } + + if (argsLen > 1) { + // Don't slice `arguments`, it prevents V8 optimizations + for (let a = 1; a < argsLen; a++) { + str += ' ' + args[a]; + } + } + + if (!this.enabled || this.level <= 0 || !str) { + return this._empty ? '' : str; + } + + // Turns out that on Windows dimmed gray text becomes invisible in cmd.exe, + // see https://github.com/chalk/chalk/issues/58 + // If we're on Windows and we're dealing with a gray color, temporarily make 'dim' a noop. + const originalDim = ansiStyles.dim.open; + if (isSimpleWindowsTerm && this.hasGrey) { + ansiStyles.dim.open = ''; + } + + for (const code of this._styles.slice().reverse()) { + // Replace any instances already present with a re-opening code + // otherwise only the part of the string until said closing code + // will be colored, and the rest will simply be 'plain'. + str = code.open + str.replace(code.closeRe, code.open) + code.close; + + // Close the styling before a linebreak and reopen + // after next line to fix a bleed issue on macOS + // https://github.com/chalk/chalk/pull/92 + str = str.replace(/\r?\n/g, `${code.close}$&${code.open}`); + } + + // Reset the original `dim` if we changed it to work around the Windows dimmed gray issue + ansiStyles.dim.open = originalDim; + + return str; +} + +function chalkTag(chalk, strings) { + if (!Array.isArray(strings)) { + // If chalk() was called by itself or with a string, + // return the string itself as a string. + return [].slice.call(arguments, 1).join(' '); + } + + const args = [].slice.call(arguments, 2); + const parts = [strings.raw[0]]; + + for (let i = 1; i < strings.length; i++) { + parts.push(String(args[i - 1]).replace(/[{}\\]/g, '\\$&')); + parts.push(String(strings.raw[i])); + } + + return template(chalk, parts.join('')); +} + +Object.defineProperties(Chalk.prototype, styles); + +module.exports = Chalk(); // eslint-disable-line new-cap +module.exports.supportsColor = stdoutColor; +module.exports.default = module.exports; // For TypeScript diff --git a/node_modules/chalk/index.js.flow b/node_modules/chalk/index.js.flow new file mode 100644 index 00000000000..622caaa2e80 --- /dev/null +++ b/node_modules/chalk/index.js.flow @@ -0,0 +1,93 @@ +// @flow strict + +type TemplateStringsArray = $ReadOnlyArray; + +export type Level = $Values<{ + None: 0, + Basic: 1, + Ansi256: 2, + TrueColor: 3 +}>; + +export type ChalkOptions = {| + enabled?: boolean, + level?: Level +|}; + +export type ColorSupport = {| + level: Level, + hasBasic: boolean, + has256: boolean, + has16m: boolean +|}; + +export interface Chalk { + (...text: string[]): string, + (text: TemplateStringsArray, ...placeholders: string[]): string, + constructor(options?: ChalkOptions): Chalk, + enabled: boolean, + level: Level, + rgb(r: number, g: number, b: number): Chalk, + hsl(h: number, s: number, l: number): Chalk, + hsv(h: number, s: number, v: number): Chalk, + hwb(h: number, w: number, b: number): Chalk, + bgHex(color: string): Chalk, + bgKeyword(color: string): Chalk, + bgRgb(r: number, g: number, b: number): Chalk, + bgHsl(h: number, s: number, l: number): Chalk, + bgHsv(h: number, s: number, v: number): Chalk, + bgHwb(h: number, w: number, b: number): Chalk, + hex(color: string): Chalk, + keyword(color: string): Chalk, + + +reset: Chalk, + +bold: Chalk, + +dim: Chalk, + +italic: Chalk, + +underline: Chalk, + +inverse: Chalk, + +hidden: Chalk, + +strikethrough: Chalk, + + +visible: Chalk, + + +black: Chalk, + +red: Chalk, + +green: Chalk, + +yellow: Chalk, + +blue: Chalk, + +magenta: Chalk, + +cyan: Chalk, + +white: Chalk, + +gray: Chalk, + +grey: Chalk, + +blackBright: Chalk, + +redBright: Chalk, + +greenBright: Chalk, + +yellowBright: Chalk, + +blueBright: Chalk, + +magentaBright: Chalk, + +cyanBright: Chalk, + +whiteBright: Chalk, + + +bgBlack: Chalk, + +bgRed: Chalk, + +bgGreen: Chalk, + +bgYellow: Chalk, + +bgBlue: Chalk, + +bgMagenta: Chalk, + +bgCyan: Chalk, + +bgWhite: Chalk, + +bgBlackBright: Chalk, + +bgRedBright: Chalk, + +bgGreenBright: Chalk, + +bgYellowBright: Chalk, + +bgBlueBright: Chalk, + +bgMagentaBright: Chalk, + +bgCyanBright: Chalk, + +bgWhiteBrigh: Chalk, + + supportsColor: ColorSupport +}; + +declare module.exports: Chalk; diff --git a/node_modules/chalk/license b/node_modules/chalk/license new file mode 100644 index 00000000000..e7af2f77107 --- /dev/null +++ b/node_modules/chalk/license @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/chalk/package.json b/node_modules/chalk/package.json new file mode 100644 index 00000000000..bc324685a76 --- /dev/null +++ b/node_modules/chalk/package.json @@ -0,0 +1,71 @@ +{ + "name": "chalk", + "version": "2.4.2", + "description": "Terminal string styling done right", + "license": "MIT", + "repository": "chalk/chalk", + "engines": { + "node": ">=4" + }, + "scripts": { + "test": "xo && tsc --project types && flow --max-warnings=0 && nyc ava", + "bench": "matcha benchmark.js", + "coveralls": "nyc report --reporter=text-lcov | coveralls" + }, + "files": [ + "index.js", + "templates.js", + "types/index.d.ts", + "index.js.flow" + ], + "keywords": [ + "color", + "colour", + "colors", + "terminal", + "console", + "cli", + "string", + "str", + "ansi", + "style", + "styles", + "tty", + "formatting", + "rgb", + "256", + "shell", + "xterm", + "log", + "logging", + "command-line", + "text" + ], + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "devDependencies": { + "ava": "*", + "coveralls": "^3.0.0", + "execa": "^0.9.0", + "flow-bin": "^0.68.0", + "import-fresh": "^2.0.0", + "matcha": "^0.7.0", + "nyc": "^11.0.2", + "resolve-from": "^4.0.0", + "typescript": "^2.5.3", + "xo": "*" + }, + "types": "types/index.d.ts", + "xo": { + "envs": [ + "node", + "mocha" + ], + "ignores": [ + "test/_flow.js" + ] + } +} diff --git a/node_modules/chalk/readme.md b/node_modules/chalk/readme.md new file mode 100644 index 00000000000..d298e2c48d6 --- /dev/null +++ b/node_modules/chalk/readme.md @@ -0,0 +1,314 @@ +

+
+
+ Chalk +
+
+
+

+ +> Terminal string styling done right + +[![Build Status](https://travis-ci.org/chalk/chalk.svg?branch=master)](https://travis-ci.org/chalk/chalk) [![Coverage Status](https://coveralls.io/repos/github/chalk/chalk/badge.svg?branch=master)](https://coveralls.io/github/chalk/chalk?branch=master) [![](https://img.shields.io/badge/unicorn-approved-ff69b4.svg)](https://www.youtube.com/watch?v=9auOCbH5Ns4) [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/xojs/xo) [![Mentioned in Awesome Node.js](https://awesome.re/mentioned-badge.svg)](https://github.com/sindresorhus/awesome-nodejs) + +### [See what's new in Chalk 2](https://github.com/chalk/chalk/releases/tag/v2.0.0) + + + + +## Highlights + +- Expressive API +- Highly performant +- Ability to nest styles +- [256/Truecolor color support](#256-and-truecolor-color-support) +- Auto-detects color support +- Doesn't extend `String.prototype` +- Clean and focused +- Actively maintained +- [Used by ~23,000 packages](https://www.npmjs.com/browse/depended/chalk) as of December 31, 2017 + + +## Install + +```console +$ npm install chalk +``` + + + + + + +## Usage + +```js +const chalk = require('chalk'); + +console.log(chalk.blue('Hello world!')); +``` + +Chalk comes with an easy to use composable API where you just chain and nest the styles you want. + +```js +const chalk = require('chalk'); +const log = console.log; + +// Combine styled and normal strings +log(chalk.blue('Hello') + ' World' + chalk.red('!')); + +// Compose multiple styles using the chainable API +log(chalk.blue.bgRed.bold('Hello world!')); + +// Pass in multiple arguments +log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz')); + +// Nest styles +log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!')); + +// Nest styles of the same type even (color, underline, background) +log(chalk.green( + 'I am a green line ' + + chalk.blue.underline.bold('with a blue substring') + + ' that becomes green again!' +)); + +// ES2015 template literal +log(` +CPU: ${chalk.red('90%')} +RAM: ${chalk.green('40%')} +DISK: ${chalk.yellow('70%')} +`); + +// ES2015 tagged template literal +log(chalk` +CPU: {red ${cpu.totalPercent}%} +RAM: {green ${ram.used / ram.total * 100}%} +DISK: {rgb(255,131,0) ${disk.used / disk.total * 100}%} +`); + +// Use RGB colors in terminal emulators that support it. +log(chalk.keyword('orange')('Yay for orange colored text!')); +log(chalk.rgb(123, 45, 67).underline('Underlined reddish color')); +log(chalk.hex('#DEADED').bold('Bold gray!')); +``` + +Easily define your own themes: + +```js +const chalk = require('chalk'); + +const error = chalk.bold.red; +const warning = chalk.keyword('orange'); + +console.log(error('Error!')); +console.log(warning('Warning!')); +``` + +Take advantage of console.log [string substitution](https://nodejs.org/docs/latest/api/console.html#console_console_log_data_args): + +```js +const name = 'Sindre'; +console.log(chalk.green('Hello %s'), name); +//=> 'Hello Sindre' +``` + + +## API + +### chalk.`
+ + Get professional support for 'camelcase' with a Tidelift subscription + +
+ + Tidelift helps make open source sustainable for maintainers while giving companies
assurances about security, maintenance, and licensing for their dependencies. +
+