Repository: opendatakit/collect
Branch: master
Commit: 77b1124ad9a2
Files: 2434
Total size: 11.5 MB
Directory structure:
gitextract_2lkczetw/
├── .circleci/
│ ├── config.yml
│ ├── generate-app-test-list.sh
│ ├── gradle-large.properties
│ ├── gradle.properties
│ └── test_modules.txt
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── CODE_OF_CONDUCT.md
│ ├── ISSUE_TEMPLATE/
│ │ └── config.yml
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── TESTING_RESULT_TEMPLATES.md
├── .gitignore
├── .hgtags
├── LICENSE.md
├── README.md
├── SECURITY.md
├── analytics/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── analytics/
│ ├── Analytics.kt
│ ├── BlockableFirebaseAnalytics.kt
│ └── NoopAnalytics.kt
├── androidshared/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── androidshared/
│ │ └── bitmap/
│ │ ├── ImageCompressorTest.kt
│ │ └── ImageFileUtilsTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── androidshared/
│ │ │ ├── async/
│ │ │ │ └── TrackableWorker.kt
│ │ │ ├── bitmap/
│ │ │ │ ├── ImageCompressor.kt
│ │ │ │ └── ImageFileUtils.kt
│ │ │ ├── data/
│ │ │ │ ├── AppState.kt
│ │ │ │ ├── Consumable.kt
│ │ │ │ └── Data.kt
│ │ │ ├── livedata/
│ │ │ │ ├── LiveDataExt.kt
│ │ │ │ ├── LiveDataUtils.java
│ │ │ │ └── NonNullLiveData.kt
│ │ │ ├── system/
│ │ │ │ ├── BroadcastReceiverRegister.kt
│ │ │ │ ├── CameraUtils.java
│ │ │ │ ├── ContextExt.kt
│ │ │ │ ├── ExternalFilesUtils.kt
│ │ │ │ ├── IntentLauncher.kt
│ │ │ │ ├── OpenGLVersionChecker.kt
│ │ │ │ ├── PlayServicesChecker.java
│ │ │ │ ├── ProcessRestoreDetector.kt
│ │ │ │ └── UriExt.kt
│ │ │ ├── ui/
│ │ │ │ ├── AlertStore.kt
│ │ │ │ ├── Animations.kt
│ │ │ │ ├── ColorPickerDialog.kt
│ │ │ │ ├── ComposeThemeProvider.kt
│ │ │ │ ├── DialogFragmentUtils.kt
│ │ │ │ ├── DialogUtils.kt
│ │ │ │ ├── DisplayString.kt
│ │ │ │ ├── EdgeToEdge.kt
│ │ │ │ ├── FragmentFactoryBuilder.kt
│ │ │ │ ├── GroupClickListener.kt
│ │ │ │ ├── ListFragmentStateAdapter.kt
│ │ │ │ ├── MenuExt.kt
│ │ │ │ ├── ObviousProgressBar.kt
│ │ │ │ ├── OneSignTextWatcher.kt
│ │ │ │ ├── PrefUtils.kt
│ │ │ │ ├── ReturnToAppActivity.kt
│ │ │ │ ├── SnackbarUtils.kt
│ │ │ │ ├── ToastUtils.kt
│ │ │ │ ├── compose/
│ │ │ │ │ └── Margins.kt
│ │ │ │ └── multiclicksafe/
│ │ │ │ ├── DoubleClickSafeMaterialButton.kt
│ │ │ │ ├── MultiClickGuard.kt
│ │ │ │ ├── MultiClickSafeMaterialButton.kt
│ │ │ │ ├── MultiClickSafeTextInputEditText.kt
│ │ │ │ └── MultiClickSaveOnClickListener.kt
│ │ │ └── utils/
│ │ │ ├── AppBarUtils.kt
│ │ │ ├── ColorUtils.kt
│ │ │ ├── CompressionUtils.kt
│ │ │ ├── FileExt.kt
│ │ │ ├── InMemUniqueIdGenerator.kt
│ │ │ ├── PathUtils.kt
│ │ │ ├── PreferenceFragmentCompatUtils.kt
│ │ │ ├── ScreenUtils.java
│ │ │ ├── SettingsUniqueIdGenerator.kt
│ │ │ ├── UniqueIdGenerator.kt
│ │ │ └── Validator.kt
│ │ └── res/
│ │ ├── color/
│ │ │ ├── color_error_button_icon.xml
│ │ │ ├── color_on_primary_low_emphasis.xml
│ │ │ ├── color_on_surface_high_emphasis.xml
│ │ │ ├── color_on_surface_low_emphasis.xml
│ │ │ ├── color_on_surface_medium_emphasis.xml
│ │ │ └── color_primary_low_emphasis.xml
│ │ ├── drawable/
│ │ │ ├── color_circle.xml
│ │ │ ├── ic_close_24.xml
│ │ │ ├── list_item_divider.xml
│ │ │ ├── radio_button_inset.xml
│ │ │ └── shadow_up.xml
│ │ ├── layout/
│ │ │ ├── app_bar_layout.xml
│ │ │ └── color_picker_dialog_layout.xml
│ │ └── values/
│ │ ├── attrs.xml
│ │ ├── color_picker_dialog_colors.xml
│ │ ├── dimens.xml
│ │ └── styles.xml
│ └── test/
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── androidshared/
│ │ ├── async/
│ │ │ └── TrackableWorkerTest.kt
│ │ ├── livedata/
│ │ │ └── LiveDataUtilsTest.kt
│ │ ├── system/
│ │ │ └── UriExtTest.kt
│ │ ├── ui/
│ │ │ ├── ColorPickerDialogTest.kt
│ │ │ ├── OneSignTextWatcherTest.kt
│ │ │ └── ReturnToAppActivityTest.kt
│ │ └── utils/
│ │ ├── ColorUtilsTest.kt
│ │ ├── CompressionUtilsTest.kt
│ │ ├── DialogFragmentUtilsTest.java
│ │ ├── InMemUniqueIdGeneratorTest.kt
│ │ ├── IntentLauncherImplTest.kt
│ │ ├── PathUtilsTest.kt
│ │ ├── SettingsUniqueIdGeneratorTest.kt
│ │ ├── UniqueIdGeneratorTest.kt
│ │ └── ValidatorTest.kt
│ └── resources/
│ └── robolectric.properties
├── androidtest/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── androidtest/
│ ├── ActivityScenarioExtensions.kt
│ ├── ActivityScenarioLauncherRule.kt
│ ├── DrawableMatcher.kt
│ ├── FakeLifecycleOwner.kt
│ ├── FragmentScenarioExtensions.kt
│ ├── LiveDataTestUtils.kt
│ ├── MainDispatcherRule.kt
│ ├── NodeInteractionExtensions.kt
│ └── RecordedIntentsRule.kt
├── async/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── async/
│ │ ├── Cancellable.kt
│ │ ├── OngoingWorkListener.kt
│ │ ├── Scheduler.kt
│ │ ├── SchedulerAsyncTaskMimic.kt
│ │ ├── SchedulerBuilder.kt
│ │ ├── ScopeCancellable.kt
│ │ ├── TaskRunner.kt
│ │ ├── TaskSpec.kt
│ │ ├── TaskSpecRunner.kt
│ │ ├── TaskSpecScheduler.kt
│ │ ├── coroutines/
│ │ │ └── CoroutineTaskRunner.kt
│ │ ├── network/
│ │ │ ├── ConnectivityProvider.kt
│ │ │ └── NetworkStateProvider.kt
│ │ ├── services/
│ │ │ └── ForegroundServiceTaskSpecRunner.kt
│ │ └── workmanager/
│ │ ├── TaskSpecWorker.kt
│ │ └── WorkManagerTaskSpecScheduler.kt
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── async/
│ ├── TaskSpecTest.kt
│ └── workmanager/
│ └── TaskSpecWorkerTest.kt
├── audio-clips/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── audioclips/
│ │ ├── AudioClipViewModel.kt
│ │ ├── AudioPlayer.kt
│ │ ├── AudioPlayerFactory.kt
│ │ ├── Clip.kt
│ │ ├── PlaybackFailedException.kt
│ │ └── ThreadSafeMediaPlayerWrapper.kt
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── audioclips/
│ └── AudioClipViewModelTest.kt
├── audio-recorder/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── audiorecorder/
│ │ ├── DaggerSetup.kt
│ │ ├── mediarecorder/
│ │ │ └── MediaRecorderRecordingResource.kt
│ │ ├── recorder/
│ │ │ ├── Recorder.kt
│ │ │ ├── RecordingResource.kt
│ │ │ └── RecordingResourceRecorder.kt
│ │ ├── recording/
│ │ │ ├── AudioRecorder.kt
│ │ │ ├── AudioRecorderFactory.kt
│ │ │ ├── AudioRecorderService.kt
│ │ │ └── internal/
│ │ │ ├── ForegroundServiceAudioRecorder.kt
│ │ │ ├── RecordingForegroundServiceNotification.kt
│ │ │ └── RecordingRepository.kt
│ │ └── testsupport/
│ │ └── StubAudioRecorder.kt
│ └── test/
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── audiorecorder/
│ │ ├── mediarecorder/
│ │ │ └── AMRRecordingResourceTest.kt
│ │ ├── recorder/
│ │ │ └── RecordingResourceRecorderTest.kt
│ │ ├── recording/
│ │ │ ├── AudioRecorderTest.kt
│ │ │ └── internal/
│ │ │ ├── AudioRecorderServiceTest.kt
│ │ │ ├── ForegroundServiceAudioRecorderTest.kt
│ │ │ └── RecordingForegroundServiceNotificationTest.kt
│ │ ├── support/
│ │ │ └── FakeRecorder.kt
│ │ └── testsupport/
│ │ ├── RobolectricApplication.kt
│ │ └── StubAudioRecorderTest.kt
│ └── resources/
│ └── robolectric.properties
├── benchmark.sh
├── build.gradle
├── check-size.sh
├── codecov.yml
├── collect_app/
│ ├── build.gradle
│ ├── google-services.json
│ ├── libs/
│ │ └── bikram-sambat-1.8.0.jar
│ ├── proguard-rules.txt
│ └── src/
│ ├── androidTest/
│ │ ├── assets/
│ │ │ └── instances/
│ │ │ ├── One Question_2021-06-22_15-55-50.xml
│ │ │ └── one-question-google_2023-08-08_14-51-00.xml
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── android/
│ │ ├── benchmark/
│ │ │ ├── EntitiesBenchmarkTest.kt
│ │ │ ├── FormsUpdateBenchmarkTest.kt
│ │ │ ├── SearchBenchmarkTest.kt
│ │ │ └── support/
│ │ │ └── Benchmarker.kt
│ │ ├── feature/
│ │ │ ├── entitymanagement/
│ │ │ │ └── ViewEntitiesTest.kt
│ │ │ ├── external/
│ │ │ │ ├── AndroidShortcutsTest.kt
│ │ │ │ ├── FormDownloadActionTest.kt
│ │ │ │ ├── FormEditActionTest.kt
│ │ │ │ ├── FormPickActionTest.kt
│ │ │ │ ├── InstanceEditActionTest.kt
│ │ │ │ ├── InstancePickActionTest.kt
│ │ │ │ └── InstanceUploadActionTest.kt
│ │ │ ├── formentry/
│ │ │ │ ├── AddRepeatTest.kt
│ │ │ │ ├── AudioAutoplayTest.kt
│ │ │ │ ├── AudioRecordingTest.java
│ │ │ │ ├── BackgroundAudioRecordingTest.java
│ │ │ │ ├── CascadingSelectTest.kt
│ │ │ │ ├── CatchFormDesignExceptionsTest.kt
│ │ │ │ ├── ContextMenuTest.java
│ │ │ │ ├── DeletingRepeatGroupsTest.java
│ │ │ │ ├── EncryptedFormTest.kt
│ │ │ │ ├── ExternalAudioRecordingTest.java
│ │ │ │ ├── ExternalSecondaryInstanceTest.java
│ │ │ │ ├── ExternalSelectsTest.kt
│ │ │ │ ├── FieldListUpdateTest.kt
│ │ │ │ ├── FillBlankFormWithRepeatGroupTest.kt
│ │ │ │ ├── FormEndTest.kt
│ │ │ │ ├── FormHierarchyTest.java
│ │ │ │ ├── FormLanguageTest.java
│ │ │ │ ├── FormMediaTest.kt
│ │ │ │ ├── FormMetadataTest.kt
│ │ │ │ ├── FormNavigationTest.kt
│ │ │ │ ├── FormSaveTest.kt
│ │ │ │ ├── FormStylingTest.kt
│ │ │ │ ├── GuidanceTest.kt
│ │ │ │ ├── ImageLoadingTest.kt
│ │ │ │ ├── IntentGroupTest.kt
│ │ │ │ ├── InvalidFormTest.kt
│ │ │ │ ├── LikertTest.java
│ │ │ │ ├── NestedIntentGroupTest.kt
│ │ │ │ ├── QuickSaveTest.java
│ │ │ │ ├── QuittingFormTest.java
│ │ │ │ ├── RankingWidgetWithCSVTest.java
│ │ │ │ ├── RequiredAndConstraintQuestionTest.kt
│ │ │ │ ├── SaveIncompleteTest.kt
│ │ │ │ ├── SavePointTest.kt
│ │ │ │ ├── SearchAppearancesTest.kt
│ │ │ │ ├── audit/
│ │ │ │ │ ├── AuditTest.kt
│ │ │ │ │ ├── IdentifyUserTest.kt
│ │ │ │ │ └── TrackChangesReasonTest.kt
│ │ │ │ ├── backgroundlocation/
│ │ │ │ │ ├── LocationTrackingAuditTest.java
│ │ │ │ │ └── SetGeopointActionTest.java
│ │ │ │ ├── dynamicpreload/
│ │ │ │ │ ├── DynamicPreLoadedDataPullTest.kt
│ │ │ │ │ └── DynamicPreLoadedDataSelects.java
│ │ │ │ └── entities/
│ │ │ │ ├── EntityFormApprovalTest.kt
│ │ │ │ ├── EntityFormCreateUpdateTest.kt
│ │ │ │ ├── EntityFormEditTest.kt
│ │ │ │ ├── EntityFormLockingTest.kt
│ │ │ │ ├── EntityFormSpecVersionTest.kt
│ │ │ │ └── EntityListSyncTest.kt
│ │ │ ├── formmanagement/
│ │ │ │ ├── BulkFinalizationTest.kt
│ │ │ │ ├── DeleteBlankFormTest.java
│ │ │ │ ├── FormUpdateTest.kt
│ │ │ │ ├── FormsAdbTest.java
│ │ │ │ ├── GetBlankFormsTest.java
│ │ │ │ ├── HideOldVersionsTest.java
│ │ │ │ ├── ManualUpdatesTest.java
│ │ │ │ ├── MatchExactlyTest.kt
│ │ │ │ └── PreviouslyDownloadedOnlyTest.kt
│ │ │ ├── instancemanagement/
│ │ │ │ ├── AutoSendTest.kt
│ │ │ │ ├── DeleteSavedFormTest.kt
│ │ │ │ ├── EditSavedFormTest.kt
│ │ │ │ ├── InstancesAdbTest.kt
│ │ │ │ ├── PartialSubmissionTest.kt
│ │ │ │ └── SendFinalizedFormTest.kt
│ │ │ ├── maps/
│ │ │ │ └── FormMapTest.kt
│ │ │ ├── projects/
│ │ │ │ ├── AddNewProjectTest.kt
│ │ │ │ ├── DeleteProjectTest.kt
│ │ │ │ ├── GoogleDriveDeprecationTest.kt
│ │ │ │ ├── LaunchScreenTest.kt
│ │ │ │ ├── MobileDeviceManagementTest.kt
│ │ │ │ ├── ProjectsAdbTest.kt
│ │ │ │ ├── SwitchProjectTest.kt
│ │ │ │ └── UpdateProjectTest.kt
│ │ │ ├── settings/
│ │ │ │ ├── ConfigureWithQRCodeTest.java
│ │ │ │ ├── FormEntrySettingsTest.kt
│ │ │ │ ├── FormManagementSettingsTest.kt
│ │ │ │ ├── FormMetadataSettingsTest.kt
│ │ │ │ ├── MovingBackwardsTest.kt
│ │ │ │ ├── ResetProjectTest.kt
│ │ │ │ ├── ServerSettingsTest.java
│ │ │ │ └── SettingLanguageTest.kt
│ │ │ └── smoke/
│ │ │ ├── AllWidgetsFormTest.kt
│ │ │ ├── BadServerTest.java
│ │ │ └── GetAndSubmitFormTest.java
│ │ ├── instrumented/
│ │ │ ├── forms/
│ │ │ │ └── FormUtilsTest.java
│ │ │ ├── tasks/
│ │ │ │ └── FormLoaderTaskTest.java
│ │ │ └── utilities/
│ │ │ ├── CustomSQLiteQueryExecutionTest.java
│ │ │ └── DateTimeUtilsTest.java
│ │ └── support/
│ │ ├── ActivityHelpers.java
│ │ ├── CollectHelpers.kt
│ │ ├── ContentProviderUtils.kt
│ │ ├── CountingTaskExecutorIdlingResource.java
│ │ ├── DummyActivityLauncher.kt
│ │ ├── FakeClickableMapFragment.kt
│ │ ├── FakeLocationClient.java
│ │ ├── FakeNetworkStateProvider.kt
│ │ ├── StorageUtils.kt
│ │ ├── StubOpenRosaServer.kt
│ │ ├── SubmissionParser.kt
│ │ ├── TestDependencies.kt
│ │ ├── TranslatedStringBuilder.kt
│ │ ├── actions/
│ │ │ └── RotateAction.java
│ │ ├── async/
│ │ │ ├── AsyncWorkTracker.kt
│ │ │ ├── AsyncWorkTrackerIdlingResource.kt
│ │ │ ├── TrackingCoroutineAndWorkManagerScheduler.kt
│ │ │ └── TrackingCoroutineDispatcher.kt
│ │ ├── matchers/
│ │ │ ├── CustomMatchers.kt
│ │ │ └── ToastMatcher.java
│ │ ├── pages/
│ │ │ ├── AboutPage.java
│ │ │ ├── AccessControlPage.kt
│ │ │ ├── AddNewRepeatDialog.kt
│ │ │ ├── AppClosedPage.kt
│ │ │ ├── AsyncPage.kt
│ │ │ ├── BlankFormSearchPage.java
│ │ │ ├── BulkFinalizationConfirmationDialogPage.kt
│ │ │ ├── CancelRecordingDialog.java
│ │ │ ├── ChangesReasonPromptPage.java
│ │ │ ├── DeleteSavedFormPage.java
│ │ │ ├── DeleteSelectedDialog.java
│ │ │ ├── EditSavedFormPage.java
│ │ │ ├── EntitiesPage.kt
│ │ │ ├── EntityListPage.kt
│ │ │ ├── ErrorDialog.kt
│ │ │ ├── ErrorPage.kt
│ │ │ ├── ExperimentalPage.java
│ │ │ ├── FillBlankFormPage.java
│ │ │ ├── FirstLaunchPage.kt
│ │ │ ├── FormEndPage.java
│ │ │ ├── FormEntryPage.java
│ │ │ ├── FormHierarchyPage.kt
│ │ │ ├── FormManagementPage.java
│ │ │ ├── FormMapPage.java
│ │ │ ├── FormMetadataPage.java
│ │ │ ├── FormsDownloadResultPage.kt
│ │ │ ├── GetBlankFormPage.java
│ │ │ ├── IdentifyUserPromptPage.java
│ │ │ ├── ListPreferenceDialog.java
│ │ │ ├── MainMenuPage.java
│ │ │ ├── MainMenuSettingsPage.kt
│ │ │ ├── ManualProjectCreatorDialogPage.kt
│ │ │ ├── MapsSettingsPage.java
│ │ │ ├── NotificationDrawer.kt
│ │ │ ├── OkDialog.java
│ │ │ ├── OpenSourceLicensesPage.java
│ │ │ ├── Page.kt
│ │ │ ├── PreferencePage.java
│ │ │ ├── ProjectDisplayPage.kt
│ │ │ ├── ProjectManagementPage.kt
│ │ │ ├── ProjectSettingsDialogPage.kt
│ │ │ ├── ProjectSettingsPage.java
│ │ │ ├── QRCodePage.java
│ │ │ ├── QrCodeProjectCreatorDialogPage.kt
│ │ │ ├── ResetApplicationDialog.java
│ │ │ ├── SaveOrDiscardFormDialog.kt
│ │ │ ├── SaveOrIgnoreDrawingDialog.kt
│ │ │ ├── SavepointRecoveryDialogPage.kt
│ │ │ ├── SelectMinimalDialogPage.kt
│ │ │ ├── SendFinalizedFormPage.kt
│ │ │ ├── ServerAuthDialog.java
│ │ │ ├── ServerSettingsPage.java
│ │ │ ├── ShortcutsPage.java
│ │ │ ├── UserAndDeviceIdentitySettingsPage.java
│ │ │ ├── UserInterfacePage.java
│ │ │ ├── ViewFormPage.kt
│ │ │ └── ViewSentFormPage.java
│ │ └── rules/
│ │ ├── BlankFormTestRule.kt
│ │ ├── CollectTestRule.kt
│ │ ├── FormEntryActivityTestRule.kt
│ │ ├── IdlingResourceRule.kt
│ │ ├── NotificationDrawerRule.kt
│ │ ├── PageComposeRule.kt
│ │ ├── PrepDeviceForTestsRule.kt
│ │ ├── RecentAppsRule.kt
│ │ ├── ResetRotationRule.kt
│ │ ├── ResetStateRule.kt
│ │ ├── RetryOnDeviceErrorRule.kt
│ │ ├── RunnableRule.kt
│ │ └── TestRuleChain.kt
│ ├── debug/
│ │ ├── AndroidManifest.xml
│ │ └── google-services.json
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── assets/
│ │ │ └── svg_map_helper.js
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ ├── android/
│ │ │ │ ├── activities/
│ │ │ │ │ ├── AboutActivity.kt
│ │ │ │ │ ├── ActivityUtils.java
│ │ │ │ │ ├── AppListActivity.java
│ │ │ │ │ ├── BearingActivity.java
│ │ │ │ │ ├── CrashHandlerActivity.kt
│ │ │ │ │ ├── DeleteFormsActivity.kt
│ │ │ │ │ ├── FirstLaunchActivity.kt
│ │ │ │ │ ├── FormDownloadListActivity.java
│ │ │ │ │ ├── FormEntryViewModelFactory.kt
│ │ │ │ │ ├── FormFillingActivity.java
│ │ │ │ │ ├── FormListActivity.java
│ │ │ │ │ ├── FormMapActivity.kt
│ │ │ │ │ ├── InstanceChooserList.java
│ │ │ │ │ ├── ScannerWithFlashlightActivity.kt
│ │ │ │ │ └── viewmodels/
│ │ │ │ │ └── FormDownloadListViewModel.java
│ │ │ │ ├── adapters/
│ │ │ │ │ ├── AboutListAdapter.kt
│ │ │ │ │ ├── AbstractSelectListAdapter.java
│ │ │ │ │ ├── FormDownloadListAdapter.java
│ │ │ │ │ ├── InstanceListCursorAdapter.java
│ │ │ │ │ ├── InstanceUploaderAdapter.java
│ │ │ │ │ ├── RankingListAdapter.java
│ │ │ │ │ ├── SelectMultipleListAdapter.java
│ │ │ │ │ └── SelectOneListAdapter.java
│ │ │ │ ├── analytics/
│ │ │ │ │ ├── AnalyticsEvents.kt
│ │ │ │ │ └── AnalyticsUtils.kt
│ │ │ │ ├── application/
│ │ │ │ │ ├── Collect.java
│ │ │ │ │ ├── CollectSettingsChangeHandler.kt
│ │ │ │ │ ├── ComposeTheme.kt
│ │ │ │ │ ├── FeatureFlags.kt
│ │ │ │ │ ├── MapboxClassInstanceCreator.kt
│ │ │ │ │ └── initialization/
│ │ │ │ │ ├── AnalyticsInitializer.kt
│ │ │ │ │ ├── ApplicationInitializer.kt
│ │ │ │ │ ├── CachedFormsCleaner.kt
│ │ │ │ │ ├── CrashReportingTree.kt
│ │ │ │ │ ├── ExistingProjectMigrator.kt
│ │ │ │ │ ├── ExistingSettingsMigrator.kt
│ │ │ │ │ ├── GoogleDriveProjectsDeleter.kt
│ │ │ │ │ ├── JavaRosaInitializer.kt
│ │ │ │ │ ├── MapsInitializer.kt
│ │ │ │ │ ├── SavepointsImporter.kt
│ │ │ │ │ ├── ScheduledWorkUpgrade.kt
│ │ │ │ │ ├── UserPropertiesInitializer.kt
│ │ │ │ │ └── upgrade/
│ │ │ │ │ ├── BeforeProjectsInstallDetector.kt
│ │ │ │ │ └── UpgradeInitializer.kt
│ │ │ │ ├── audio/
│ │ │ │ │ ├── AMRAppender.java
│ │ │ │ │ ├── AudioButton.java
│ │ │ │ │ ├── AudioControllerView.java
│ │ │ │ │ ├── AudioFileAppender.java
│ │ │ │ │ ├── AudioRecordingControllerFragment.java
│ │ │ │ │ ├── AudioRecordingErrorDialogFragment.java
│ │ │ │ │ ├── BackgroundAudioHelpDialogFragment.java
│ │ │ │ │ ├── M4AAppender.java
│ │ │ │ │ ├── VolumeBar.java
│ │ │ │ │ └── Waveform.java
│ │ │ │ ├── backgroundwork/
│ │ │ │ │ ├── AutoUpdateTaskSpec.kt
│ │ │ │ │ ├── BackgroundWorkUtils.java
│ │ │ │ │ ├── FormUpdateAndInstanceSubmitScheduler.java
│ │ │ │ │ ├── FormUpdateScheduler.java
│ │ │ │ │ ├── InstanceSubmitScheduler.java
│ │ │ │ │ ├── SendFormsTaskSpec.kt
│ │ │ │ │ ├── SyncFormsTaskSpec.kt
│ │ │ │ │ └── TaskData.kt
│ │ │ │ ├── configure/
│ │ │ │ │ └── qr/
│ │ │ │ │ ├── AppConfigurationGenerator.kt
│ │ │ │ │ ├── CachingQRCodeGenerator.kt
│ │ │ │ │ ├── QRCodeActivityResultDelegate.kt
│ │ │ │ │ ├── QRCodeMenuProvider.kt
│ │ │ │ │ ├── QRCodeScannerFragment.kt
│ │ │ │ │ ├── QRCodeTabsActivity.kt
│ │ │ │ │ ├── QRCodeViewModel.kt
│ │ │ │ │ ├── SettingsBarcodeScannerViewFactory.kt
│ │ │ │ │ └── ShowQRCodeFragment.kt
│ │ │ │ ├── dao/
│ │ │ │ │ ├── CursorLoaderFactory.java
│ │ │ │ │ └── helpers/
│ │ │ │ │ └── InstancesDaoHelper.java
│ │ │ │ ├── database/
│ │ │ │ │ ├── DatabaseConstants.java
│ │ │ │ │ ├── DatabaseObjectMapper.kt
│ │ │ │ │ ├── entities/
│ │ │ │ │ │ └── DatabaseEntitiesRepository.kt
│ │ │ │ │ ├── forms/
│ │ │ │ │ │ ├── DatabaseFormColumns.kt
│ │ │ │ │ │ ├── DatabaseFormsRepository.java
│ │ │ │ │ │ └── FormDatabaseMigrator.java
│ │ │ │ │ ├── instances/
│ │ │ │ │ │ ├── DatabaseInstanceColumns.kt
│ │ │ │ │ │ ├── DatabaseInstancesRepository.java
│ │ │ │ │ │ └── InstanceDatabaseMigrator.java
│ │ │ │ │ ├── itemsets/
│ │ │ │ │ │ └── DatabaseFastExternalItemsetsRepository.kt
│ │ │ │ │ └── savepoints/
│ │ │ │ │ ├── DatabaseSavepointsColumns.kt
│ │ │ │ │ ├── DatabaseSavepointsRepository.kt
│ │ │ │ │ └── SavepointsDatabaseMigrator.kt
│ │ │ │ ├── dynamicpreload/
│ │ │ │ │ ├── DynamicPreloadXFormParserFactory.kt
│ │ │ │ │ ├── ExternalAnswerResolver.java
│ │ │ │ │ ├── ExternalAppsUtils.java
│ │ │ │ │ ├── ExternalDataHandler.java
│ │ │ │ │ ├── ExternalDataManager.java
│ │ │ │ │ ├── ExternalDataManagerImpl.java
│ │ │ │ │ ├── ExternalDataReader.java
│ │ │ │ │ ├── ExternalDataReaderImpl.java
│ │ │ │ │ ├── ExternalDataUseCases.kt
│ │ │ │ │ ├── ExternalDataUtil.java
│ │ │ │ │ ├── ExternalSQLiteOpenHelper.java
│ │ │ │ │ ├── ExternalSelectChoice.java
│ │ │ │ │ └── handler/
│ │ │ │ │ ├── ExternalDataHandlerBase.java
│ │ │ │ │ ├── ExternalDataHandlerPull.java
│ │ │ │ │ ├── ExternalDataHandlerSearch.java
│ │ │ │ │ └── ExternalDataSearchType.java
│ │ │ │ ├── entities/
│ │ │ │ │ └── EntitiesRepositoryProvider.kt
│ │ │ │ ├── exception/
│ │ │ │ │ ├── EncryptionException.java
│ │ │ │ │ ├── ExternalDataException.java
│ │ │ │ │ ├── ExternalParamsException.java
│ │ │ │ │ └── JavaRosaException.java
│ │ │ │ ├── external/
│ │ │ │ │ ├── AndroidShortcutsActivity.kt
│ │ │ │ │ ├── FormUriActivity.kt
│ │ │ │ │ ├── FormsContract.java
│ │ │ │ │ ├── FormsProvider.java
│ │ │ │ │ ├── InstanceProvider.java
│ │ │ │ │ └── InstancesContract.java
│ │ │ │ ├── fastexternalitemset/
│ │ │ │ │ ├── ItemsetDao.java
│ │ │ │ │ ├── ItemsetDbAdapter.java
│ │ │ │ │ └── XPathParseTool.java
│ │ │ │ ├── formentry/
│ │ │ │ │ ├── BackgroundAudioPermissionDialogFragment.java
│ │ │ │ │ ├── BackgroundAudioViewModel.java
│ │ │ │ │ ├── CurrentFormIndex.kt
│ │ │ │ │ ├── FormAnimation.kt
│ │ │ │ │ ├── FormDefCache.kt
│ │ │ │ │ ├── FormEnd.kt
│ │ │ │ │ ├── FormEndView.kt
│ │ │ │ │ ├── FormEndViewModel.kt
│ │ │ │ │ ├── FormEntryMenuProvider.kt
│ │ │ │ │ ├── FormEntryUseCases.kt
│ │ │ │ │ ├── FormEntryViewModel.java
│ │ │ │ │ ├── FormError.kt
│ │ │ │ │ ├── FormIndexAnimationHandler.kt
│ │ │ │ │ ├── FormLoadingDialogFragment.java
│ │ │ │ │ ├── FormOpeningMode.kt
│ │ │ │ │ ├── FormSessionRepository.kt
│ │ │ │ │ ├── ODKView.java
│ │ │ │ │ ├── PrinterWidgetViewModel.kt
│ │ │ │ │ ├── QuitFormDialog.kt
│ │ │ │ │ ├── RecordingHandler.java
│ │ │ │ │ ├── RecordingWarningDialogFragment.java
│ │ │ │ │ ├── RefreshFormListDialogFragment.java
│ │ │ │ │ ├── SwipeHandler.kt
│ │ │ │ │ ├── audit/
│ │ │ │ │ │ ├── AsyncTaskAuditEventWriter.java
│ │ │ │ │ │ ├── AuditConfig.java
│ │ │ │ │ │ ├── AuditEvent.java
│ │ │ │ │ │ ├── AuditEventCSVLine.java
│ │ │ │ │ │ ├── AuditEventLogger.java
│ │ │ │ │ │ ├── AuditEventSaveTask.java
│ │ │ │ │ │ ├── AuditUtils.kt
│ │ │ │ │ │ ├── ChangesReasonPromptDialogFragment.java
│ │ │ │ │ │ ├── IdentifyUserPromptDialogFragment.java
│ │ │ │ │ │ └── IdentityPromptViewModel.java
│ │ │ │ │ ├── backgroundlocation/
│ │ │ │ │ │ ├── BackgroundLocationHelper.java
│ │ │ │ │ │ ├── BackgroundLocationManager.java
│ │ │ │ │ │ └── BackgroundLocationViewModel.java
│ │ │ │ │ ├── media/
│ │ │ │ │ │ ├── FormMediaUtils.java
│ │ │ │ │ │ └── PromptAutoplayer.java
│ │ │ │ │ ├── questions/
│ │ │ │ │ │ ├── AnswersProvider.java
│ │ │ │ │ │ ├── AudioVideoImageTextLabel.java
│ │ │ │ │ │ ├── NoButtonsItem.java
│ │ │ │ │ │ ├── QuestionDetails.java
│ │ │ │ │ │ └── SelectChoiceUtils.kt
│ │ │ │ │ ├── repeats/
│ │ │ │ │ │ ├── AddRepeatDialog.kt
│ │ │ │ │ │ └── DeleteRepeatDialogFragment.java
│ │ │ │ │ └── saving/
│ │ │ │ │ ├── DiskFormSaver.java
│ │ │ │ │ ├── FormSaveViewModel.java
│ │ │ │ │ ├── FormSaver.java
│ │ │ │ │ ├── SaveAnswerFileErrorDialogFragment.java
│ │ │ │ │ ├── SaveAnswerFileProgressDialogFragment.java
│ │ │ │ │ └── SaveFormProgressDialogFragment.java
│ │ │ │ ├── formhierarchy/
│ │ │ │ │ ├── FormHierarchyFragment.java
│ │ │ │ │ ├── FormHierarchyFragmentHostActivity.kt
│ │ │ │ │ ├── FormHierarchyViewModel.kt
│ │ │ │ │ ├── HierarchyItem.kt
│ │ │ │ │ ├── HierarchyListAdapter.kt
│ │ │ │ │ ├── HierarchyListItemView.kt
│ │ │ │ │ └── QuestionAnswerProcessor.kt
│ │ │ │ ├── formlists/
│ │ │ │ │ ├── blankformlist/
│ │ │ │ │ │ ├── BlankFormListActivity.kt
│ │ │ │ │ │ ├── BlankFormListAdapter.kt
│ │ │ │ │ │ ├── BlankFormListItem.kt
│ │ │ │ │ │ ├── BlankFormListItemView.kt
│ │ │ │ │ │ ├── BlankFormListMenuProvider.kt
│ │ │ │ │ │ ├── BlankFormListViewModel.kt
│ │ │ │ │ │ └── DeleteBlankFormFragment.kt
│ │ │ │ │ ├── savedformlist/
│ │ │ │ │ │ ├── DeleteSavedFormFragment.kt
│ │ │ │ │ │ ├── SavedFormListItemView.kt
│ │ │ │ │ │ ├── SavedFormListListMenuProvider.kt
│ │ │ │ │ │ ├── SavedFormListViewModel.kt
│ │ │ │ │ │ └── SelectableSavedFormListItemViewHolder.kt
│ │ │ │ │ └── sorting/
│ │ │ │ │ ├── FormListSortingAdapter.kt
│ │ │ │ │ ├── FormListSortingBottomSheetDialog.kt
│ │ │ │ │ └── FormListSortingOption.kt
│ │ │ │ ├── formmanagement/
│ │ │ │ │ ├── CollectFormEntryControllerFactory.kt
│ │ │ │ │ ├── FormFillingIntentFactory.kt
│ │ │ │ │ ├── FormSourceExceptionMapper.kt
│ │ │ │ │ ├── FormsDataService.kt
│ │ │ │ │ ├── LocalFormUseCases.kt
│ │ │ │ │ ├── OpenRosaClientProvider.kt
│ │ │ │ │ ├── ServerFormDetails.kt
│ │ │ │ │ ├── ServerFormUseCases.kt
│ │ │ │ │ ├── download/
│ │ │ │ │ │ ├── FormDownloadException.kt
│ │ │ │ │ │ ├── FormDownloadExceptionMapper.kt
│ │ │ │ │ │ ├── FormDownloader.kt
│ │ │ │ │ │ └── ServerFormDownloader.java
│ │ │ │ │ ├── drafts/
│ │ │ │ │ │ ├── BulkFinalizationViewModel.kt
│ │ │ │ │ │ └── DraftsMenuProvider.kt
│ │ │ │ │ ├── finalization/
│ │ │ │ │ │ └── EditedFormFinalizationProcessor.kt
│ │ │ │ │ ├── formmap/
│ │ │ │ │ │ └── FormMapViewModel.kt
│ │ │ │ │ ├── matchexactly/
│ │ │ │ │ │ └── ServerFormsSynchronizer.java
│ │ │ │ │ └── metadata/
│ │ │ │ │ ├── FormMetadata.kt
│ │ │ │ │ └── FormMetadataParser.kt
│ │ │ │ ├── fragments/
│ │ │ │ │ ├── BarCodeScannerFragment.kt
│ │ │ │ │ ├── BarcodeWidgetScannerFragment.kt
│ │ │ │ │ ├── MediaLoadingFragment.java
│ │ │ │ │ ├── dialogs/
│ │ │ │ │ │ ├── FormsDownloadResultDialog.kt
│ │ │ │ │ │ ├── LocationProvidersDisabledDialog.java
│ │ │ │ │ │ ├── MovingBackwardsDialog.java
│ │ │ │ │ │ ├── RangePickerDialogFragment.kt
│ │ │ │ │ │ ├── RankingWidgetDialog.java
│ │ │ │ │ │ ├── ResetSettingsResultDialog.java
│ │ │ │ │ │ ├── SelectMinimalDialog.java
│ │ │ │ │ │ ├── SelectMultiMinimalDialog.java
│ │ │ │ │ │ ├── SelectOneMinimalDialog.java
│ │ │ │ │ │ └── SimpleDialog.java
│ │ │ │ │ └── viewmodels/
│ │ │ │ │ ├── RankingViewModel.java
│ │ │ │ │ └── SelectMinimalViewModel.java
│ │ │ │ ├── geo/
│ │ │ │ │ ├── MapConfiguratorProvider.java
│ │ │ │ │ └── MapFragmentFactoryImpl.kt
│ │ │ │ ├── injection/
│ │ │ │ │ ├── DaggerUtils.kt
│ │ │ │ │ └── config/
│ │ │ │ │ ├── AppDependencyComponent.kt
│ │ │ │ │ ├── AppDependencyModule.java
│ │ │ │ │ ├── CollectDrawDependencyModule.kt
│ │ │ │ │ ├── CollectEntitiesDependencyModule.kt
│ │ │ │ │ ├── CollectGeoDependencyModule.kt
│ │ │ │ │ ├── CollectGoogleMapsDependencyModule.kt
│ │ │ │ │ ├── CollectOsmDroidDependencyModule.kt
│ │ │ │ │ ├── CollectProjectsDependencyModule.kt
│ │ │ │ │ ├── CollectSelfieCameraDependencyModule.kt
│ │ │ │ │ └── ProjectDependencyModuleFactory.kt
│ │ │ │ ├── instancemanagement/
│ │ │ │ │ ├── FinalizeAllSnackbarPresenter.kt
│ │ │ │ │ ├── InstanceDeleter.kt
│ │ │ │ │ ├── InstanceDiskSynchronizer.java
│ │ │ │ │ ├── InstanceExt.kt
│ │ │ │ │ ├── InstanceListItemView.kt
│ │ │ │ │ ├── InstanceSubmitter.kt
│ │ │ │ │ ├── InstancesDataService.kt
│ │ │ │ │ ├── LocalInstancesUseCases.kt
│ │ │ │ │ ├── autosend/
│ │ │ │ │ │ ├── AutoSendSettingsProvider.kt
│ │ │ │ │ │ ├── FormExt.kt
│ │ │ │ │ │ └── InstanceAutoSendFetcher.kt
│ │ │ │ │ └── send/
│ │ │ │ │ ├── InstanceUploaderActivity.java
│ │ │ │ │ ├── InstanceUploaderListActivity.java
│ │ │ │ │ ├── ReadyToSendBanner.kt
│ │ │ │ │ └── ReadyToSendViewModel.kt
│ │ │ │ ├── itemsets/
│ │ │ │ │ └── FastExternalItemsetsRepository.kt
│ │ │ │ ├── javarosawrapper/
│ │ │ │ │ ├── FormController.kt
│ │ │ │ │ ├── FormControllerExt.kt
│ │ │ │ │ ├── FormDesignException.kt
│ │ │ │ │ ├── FormIndexUtils.java
│ │ │ │ │ ├── InstanceMetadata.java
│ │ │ │ │ ├── JavaRosaFormController.java
│ │ │ │ │ └── ValidationResult.kt
│ │ │ │ ├── listeners/
│ │ │ │ │ ├── AdvanceToNextListener.java
│ │ │ │ │ ├── DeleteInstancesListener.java
│ │ │ │ │ ├── DownloadFormsTaskListener.java
│ │ │ │ │ ├── FormListDownloaderListener.java
│ │ │ │ │ ├── FormLoaderListener.java
│ │ │ │ │ ├── InstanceUploaderListener.java
│ │ │ │ │ ├── Result.java
│ │ │ │ │ ├── SelectItemClickListener.java
│ │ │ │ │ ├── ThousandsSeparatorTextWatcher.java
│ │ │ │ │ └── WidgetValueChangedListener.java
│ │ │ │ ├── location/
│ │ │ │ │ └── client/
│ │ │ │ │ └── MaxAccuracyWithinTimeoutLocationClientWrapper.java
│ │ │ │ ├── logic/
│ │ │ │ │ ├── FileReference.java
│ │ │ │ │ ├── FileReferenceFactory.java
│ │ │ │ │ ├── ImmutableDisplayableQuestion.java
│ │ │ │ │ └── actions/
│ │ │ │ │ └── setgeopoint/
│ │ │ │ │ ├── CollectSetGeopointAction.java
│ │ │ │ │ └── CollectSetGeopointActionHandler.java
│ │ │ │ ├── mainmenu/
│ │ │ │ │ ├── CurrentProjectViewModel.kt
│ │ │ │ │ ├── MainMenuActivity.kt
│ │ │ │ │ ├── MainMenuButton.kt
│ │ │ │ │ ├── MainMenuFragment.kt
│ │ │ │ │ ├── MainMenuViewModel.kt
│ │ │ │ │ ├── MainMenuViewModelFactory.kt
│ │ │ │ │ ├── PermissionsDialogFragment.kt
│ │ │ │ │ ├── RequestPermissionsViewModel.kt
│ │ │ │ │ └── StartNewFormButton.kt
│ │ │ │ ├── notifications/
│ │ │ │ │ ├── NotificationManagerNotifier.kt
│ │ │ │ │ ├── NotificationUtils.kt
│ │ │ │ │ ├── Notifier.kt
│ │ │ │ │ └── builders/
│ │ │ │ │ ├── FormUpdatesAvailableNotificationBuilder.kt
│ │ │ │ │ ├── FormUpdatesDownloadedNotificationBuilder.kt
│ │ │ │ │ ├── FormsSubmissionNotificationBuilder.kt
│ │ │ │ │ ├── FormsSyncFailedNotificationBuilder.kt
│ │ │ │ │ └── FormsSyncStoppedNotificationBuilder.kt
│ │ │ │ ├── preferences/
│ │ │ │ │ ├── Defaults.kt
│ │ │ │ │ ├── PreferenceVisibilityHandler.kt
│ │ │ │ │ ├── ProjectPreferencesViewModel.kt
│ │ │ │ │ ├── ServerPreferencesAdder.java
│ │ │ │ │ ├── SettingsExt.kt
│ │ │ │ │ ├── dialogs/
│ │ │ │ │ │ ├── AdminPasswordDialogFragment.kt
│ │ │ │ │ │ ├── ChangeAdminPasswordDialog.kt
│ │ │ │ │ │ ├── DeleteProjectDialog.kt
│ │ │ │ │ │ ├── ResetDialogPreference.java
│ │ │ │ │ │ ├── ResetDialogPreferenceFragmentCompat.java
│ │ │ │ │ │ ├── ResetProgressDialog.kt
│ │ │ │ │ │ └── ServerAuthDialogFragment.java
│ │ │ │ │ ├── filters/
│ │ │ │ │ │ └── ControlCharacterFilter.java
│ │ │ │ │ ├── screens/
│ │ │ │ │ │ ├── AccessControlPreferencesFragment.kt
│ │ │ │ │ │ ├── BaseAdminPreferencesFragment.java
│ │ │ │ │ │ ├── BasePreferencesFragment.kt
│ │ │ │ │ │ ├── BaseProjectPreferencesFragment.java
│ │ │ │ │ │ ├── DevToolsPreferencesFragment.kt
│ │ │ │ │ │ ├── ExperimentalPreferencesFragment.java
│ │ │ │ │ │ ├── FormEntryAccessPreferencesFragment.kt
│ │ │ │ │ │ ├── FormManagementPreferencesFragment.java
│ │ │ │ │ │ ├── FormMetadataPreferencesFragment.java
│ │ │ │ │ │ ├── IdentityPreferencesFragment.kt
│ │ │ │ │ │ ├── MainMenuAccessPreferencesFragment.kt
│ │ │ │ │ │ ├── MapsPreferencesFragment.kt
│ │ │ │ │ │ ├── ProjectDisplayPreferencesFragment.kt
│ │ │ │ │ │ ├── ProjectManagementPreferencesFragment.kt
│ │ │ │ │ │ ├── ProjectPreferencesActivity.kt
│ │ │ │ │ │ ├── ProjectPreferencesFragment.kt
│ │ │ │ │ │ ├── ServerPreferencesFragment.java
│ │ │ │ │ │ ├── UserInterfacePreferencesFragment.java
│ │ │ │ │ │ └── UserSettingsAccessPreferencesFragment.java
│ │ │ │ │ ├── source/
│ │ │ │ │ │ ├── SettingsStore.kt
│ │ │ │ │ │ ├── SharedPreferencesSettings.kt
│ │ │ │ │ │ └── SharedPreferencesSettingsProvider.kt
│ │ │ │ │ └── utilities/
│ │ │ │ │ └── PreferencesUtils.java
│ │ │ │ ├── projects/
│ │ │ │ │ ├── DuplicateProjectConfirmationDialog.kt
│ │ │ │ │ ├── FileDebugLogger.kt
│ │ │ │ │ ├── ManualProjectCreatorDialog.kt
│ │ │ │ │ ├── ProjectCreatorImpl.kt
│ │ │ │ │ ├── ProjectDeleter.kt
│ │ │ │ │ ├── ProjectDependencyModule.kt
│ │ │ │ │ ├── ProjectIconView.kt
│ │ │ │ │ ├── ProjectListItemView.kt
│ │ │ │ │ ├── ProjectResetter.kt
│ │ │ │ │ ├── ProjectSettingsDialog.kt
│ │ │ │ │ ├── ProjectsDataService.kt
│ │ │ │ │ ├── QrCodeProjectCreatorDialog.kt
│ │ │ │ │ └── SettingsConnectionMatcherImpl.kt
│ │ │ │ ├── savepoints/
│ │ │ │ │ ├── SavepointTask.kt
│ │ │ │ │ └── SavepointUseCases.kt
│ │ │ │ ├── state/
│ │ │ │ │ └── DataKeys.kt
│ │ │ │ ├── storage/
│ │ │ │ │ ├── StoragePathProvider.kt
│ │ │ │ │ ├── StoragePaths.kt
│ │ │ │ │ └── StorageSubdirectory.java
│ │ │ │ ├── tasks/
│ │ │ │ │ ├── DownloadFormListTask.java
│ │ │ │ │ ├── DownloadFormsTask.java
│ │ │ │ │ ├── FormLoaderTask.java
│ │ │ │ │ ├── InstanceUploaderTask.java
│ │ │ │ │ ├── MediaLoadingTask.java
│ │ │ │ │ ├── ProgressNotifier.java
│ │ │ │ │ ├── SaveFormIndexTask.java
│ │ │ │ │ ├── SaveFormToDisk.java
│ │ │ │ │ └── SaveToDiskResult.java
│ │ │ │ ├── upload/
│ │ │ │ │ ├── FormUploadException.kt
│ │ │ │ │ ├── InstanceServerUploader.java
│ │ │ │ │ └── InstanceUploader.java
│ │ │ │ ├── utilities/
│ │ │ │ │ ├── ActionRegister.kt
│ │ │ │ │ ├── AdminPasswordProvider.java
│ │ │ │ │ ├── AndroidUserAgent.java
│ │ │ │ │ ├── AnimationUtils.java
│ │ │ │ │ ├── Appearances.kt
│ │ │ │ │ ├── ApplicationConstants.java
│ │ │ │ │ ├── ArrayUtils.java
│ │ │ │ │ ├── AuthDialogUtility.java
│ │ │ │ │ ├── CSVUtils.java
│ │ │ │ │ ├── ChangeLockProvider.kt
│ │ │ │ │ ├── CollectStrictMode.kt
│ │ │ │ │ ├── ContentUriHelper.kt
│ │ │ │ │ ├── ContentUriProvider.java
│ │ │ │ │ ├── ControllableLifecyleOwner.kt
│ │ │ │ │ ├── DialogUtils.java
│ │ │ │ │ ├── EncryptionUtils.java
│ │ │ │ │ ├── ExternalAppIntentProvider.java
│ │ │ │ │ ├── ExternalizableFormDefCache.java
│ │ │ │ │ ├── FileProvider.java
│ │ │ │ │ ├── FileUtils.java
│ │ │ │ │ ├── FormEntryPromptUtils.java
│ │ │ │ │ ├── FormNameUtils.java
│ │ │ │ │ ├── FormUtils.java
│ │ │ │ │ ├── FormsDownloadResultInterpreter.kt
│ │ │ │ │ ├── FormsRepositoryProvider.kt
│ │ │ │ │ ├── FormsUploadResultInterpreter.kt
│ │ │ │ │ ├── HtmlUtils.java
│ │ │ │ │ ├── ImageCompressionController.kt
│ │ │ │ │ ├── InstanceAutoDeleteChecker.kt
│ │ │ │ │ ├── InstanceUploaderUtils.java
│ │ │ │ │ ├── InstancesRepositoryProvider.kt
│ │ │ │ │ ├── LocaleHelper.kt
│ │ │ │ │ ├── MediaUtils.kt
│ │ │ │ │ ├── MyanmarDateUtils.java
│ │ │ │ │ ├── QuestionMediaManager.java
│ │ │ │ │ ├── RankingItemTouchHelperCallback.java
│ │ │ │ │ ├── ReplaceCallback.java
│ │ │ │ │ ├── ResponseMessageParser.java
│ │ │ │ │ ├── SavepointsRepositoryProvider.kt
│ │ │ │ │ ├── SelectOneWidgetUtils.java
│ │ │ │ │ ├── SoftKeyboardController.kt
│ │ │ │ │ ├── ThemeUtils.java
│ │ │ │ │ ├── UnderlyingValuesConcat.java
│ │ │ │ │ ├── ViewUtils.kt
│ │ │ │ │ ├── WebCredentialsUtils.java
│ │ │ │ │ └── ZipUtils.java
│ │ │ │ ├── version/
│ │ │ │ │ ├── VersionDescriptionProvider.java
│ │ │ │ │ └── VersionInformation.java
│ │ │ │ ├── views/
│ │ │ │ │ ├── ChoicesRecyclerView.java
│ │ │ │ │ ├── CustomNumberPicker.kt
│ │ │ │ │ ├── CustomWebView.java
│ │ │ │ │ ├── DayNightProgressDialog.java
│ │ │ │ │ ├── DecoratedBarcodeView.kt
│ │ │ │ │ ├── SlidingTabLayout.java
│ │ │ │ │ ├── SlidingTabStrip.java
│ │ │ │ │ ├── TrackingTouchSlider.kt
│ │ │ │ │ ├── TransparentProgressScreen.kt
│ │ │ │ │ ├── TwoItemMultipleChoiceView.java
│ │ │ │ │ └── WidgetAnswerText.kt
│ │ │ │ └── widgets/
│ │ │ │ ├── AnnotateWidget.java
│ │ │ │ ├── AudioWidget.java
│ │ │ │ ├── BaseImageWidget.java
│ │ │ │ ├── BearingWidget.java
│ │ │ │ ├── CounterWidget.kt
│ │ │ │ ├── DecimalWidget.java
│ │ │ │ ├── DrawWidget.java
│ │ │ │ ├── ExAudioWidget.java
│ │ │ │ ├── ExDecimalWidget.java
│ │ │ │ ├── ExImageWidget.java
│ │ │ │ ├── ExIntegerWidget.java
│ │ │ │ ├── ExStringWidget.java
│ │ │ │ ├── GeoPointMapWidget.java
│ │ │ │ ├── GeoPointWidget.java
│ │ │ │ ├── GeoShapeWidget.java
│ │ │ │ ├── GeoTraceWidget.java
│ │ │ │ ├── ImageWidget.java
│ │ │ │ ├── IntegerWidget.java
│ │ │ │ ├── MediaWidgetAnswerViewModel.kt
│ │ │ │ ├── OSMWidget.java
│ │ │ │ ├── PrinterWidget.kt
│ │ │ │ ├── QuestionWidget.java
│ │ │ │ ├── RatingWidget.java
│ │ │ │ ├── SignatureWidget.java
│ │ │ │ ├── StringNumberWidget.java
│ │ │ │ ├── StringWidget.java
│ │ │ │ ├── TextWidgetAnswer.kt
│ │ │ │ ├── TimedGridWidget.kt
│ │ │ │ ├── TriggerWidget.java
│ │ │ │ ├── UrlWidget.java
│ │ │ │ ├── WidgetAnswer.kt
│ │ │ │ ├── WidgetAnswerView.kt
│ │ │ │ ├── WidgetFactory.java
│ │ │ │ ├── WidgetIconButton.kt
│ │ │ │ ├── arbitraryfile/
│ │ │ │ │ ├── ArbitraryFileWidget.kt
│ │ │ │ │ ├── ArbitraryFileWidgetContent.kt
│ │ │ │ │ ├── ArbitraryFileWidgetDelegate.kt
│ │ │ │ │ ├── ExArbitraryFileWidget.kt
│ │ │ │ │ └── ExArbitraryFileWidgetContent.kt
│ │ │ │ ├── barcode/
│ │ │ │ │ ├── BarcodeWidget.kt
│ │ │ │ │ └── BarcodeWidgetContent.kt
│ │ │ │ ├── datetime/
│ │ │ │ │ ├── DatePickerDetails.java
│ │ │ │ │ ├── DateTimeUtils.java
│ │ │ │ │ ├── DateTimeWidget.java
│ │ │ │ │ ├── DateWidget.java
│ │ │ │ │ ├── TimeWidget.java
│ │ │ │ │ └── pickers/
│ │ │ │ │ ├── BikramSambatDatePickerDialog.java
│ │ │ │ │ ├── BuddhistDatePickerDialog.kt
│ │ │ │ │ ├── CopticDatePickerDialog.java
│ │ │ │ │ ├── CustomDatePickerDialog.java
│ │ │ │ │ ├── CustomTimePickerDialog.java
│ │ │ │ │ ├── EthiopianDatePickerDialog.java
│ │ │ │ │ ├── FixedDatePickerDialog.java
│ │ │ │ │ ├── IslamicDatePickerDialog.java
│ │ │ │ │ ├── MyanmarDatePickerDialog.java
│ │ │ │ │ └── PersianDatePickerDialog.java
│ │ │ │ ├── interfaces/
│ │ │ │ │ ├── FileWidget.java
│ │ │ │ │ ├── GeoDataRequester.kt
│ │ │ │ │ ├── MultiChoiceWidget.java
│ │ │ │ │ ├── Printer.kt
│ │ │ │ │ ├── SelectChoiceLoader.kt
│ │ │ │ │ ├── Widget.java
│ │ │ │ │ └── WidgetDataReceiver.kt
│ │ │ │ ├── items/
│ │ │ │ │ ├── BaseSelectListWidget.java
│ │ │ │ │ ├── ItemsWidgetUtils.kt
│ │ │ │ │ ├── LabelWidget.java
│ │ │ │ │ ├── LikertWidget.java
│ │ │ │ │ ├── ListMultiWidget.java
│ │ │ │ │ ├── ListWidget.java
│ │ │ │ │ ├── RankingWidget.java
│ │ │ │ │ ├── SelectImageMapWidget.java
│ │ │ │ │ ├── SelectMinimalWidget.java
│ │ │ │ │ ├── SelectMultiImageMapWidget.java
│ │ │ │ │ ├── SelectMultiMinimalWidget.java
│ │ │ │ │ ├── SelectMultiWidget.java
│ │ │ │ │ ├── SelectOneFromMapDialogFragment.kt
│ │ │ │ │ ├── SelectOneFromMapWidget.kt
│ │ │ │ │ ├── SelectOneImageMapWidget.java
│ │ │ │ │ ├── SelectOneMinimalWidget.java
│ │ │ │ │ └── SelectOneWidget.java
│ │ │ │ ├── range/
│ │ │ │ │ ├── RangeDecimalWidget.java
│ │ │ │ │ ├── RangeIntegerWidget.java
│ │ │ │ │ ├── RangePickerWidget.java
│ │ │ │ │ └── RangePickerWidgetUtils.kt
│ │ │ │ ├── utilities/
│ │ │ │ │ ├── ActivityGeoDataRequester.kt
│ │ │ │ │ ├── AdditionalAttributes.kt
│ │ │ │ │ ├── AudioFileRequester.java
│ │ │ │ │ ├── AudioRecorderRecordingStatusHandler.java
│ │ │ │ │ ├── BindAttributes.kt
│ │ │ │ │ ├── DateTimeWidgetUtils.java
│ │ │ │ │ ├── ExternalAppRecordingRequester.kt
│ │ │ │ │ ├── FileRequester.kt
│ │ │ │ │ ├── FormControllerWaitingForDataRegistry.java
│ │ │ │ │ ├── GeoPolyDialogFragment.kt
│ │ │ │ │ ├── GeoWidgetUtils.kt
│ │ │ │ │ ├── GetContentAudioFileRequester.kt
│ │ │ │ │ ├── ImageCaptureIntentCreator.kt
│ │ │ │ │ ├── InternalRecordingRequester.java
│ │ │ │ │ ├── QuestionFontSizeUtils.kt
│ │ │ │ │ ├── RangeWidgetUtils.java
│ │ │ │ │ ├── RecordingRequester.java
│ │ │ │ │ ├── RecordingRequesterProvider.java
│ │ │ │ │ ├── RecordingStatusHandler.java
│ │ │ │ │ ├── SearchQueryViewModel.java
│ │ │ │ │ ├── StringRequester.kt
│ │ │ │ │ ├── StringWidgetUtils.java
│ │ │ │ │ ├── ViewModelAudioPlayer.kt
│ │ │ │ │ ├── WaitingForDataRegistry.java
│ │ │ │ │ └── WidgetAnswerDialogFragment.kt
│ │ │ │ ├── video/
│ │ │ │ │ ├── ExVideoWidget.kt
│ │ │ │ │ ├── ExVideoWidgetContent.kt
│ │ │ │ │ ├── VideoWidget.kt
│ │ │ │ │ ├── VideoWidgetAnswer.kt
│ │ │ │ │ └── VideoWidgetContent.kt
│ │ │ │ ├── viewmodels/
│ │ │ │ │ ├── DateTimeViewModel.java
│ │ │ │ │ └── QuestionViewModel.kt
│ │ │ │ └── warnings/
│ │ │ │ └── SpacesInUnderlyingValuesWarning.java
│ │ │ └── utilities/
│ │ │ ├── Result.java
│ │ │ └── UserAgentProvider.java
│ │ └── res/
│ │ ├── anim/
│ │ │ ├── fade_in.xml
│ │ │ ├── fade_out.xml
│ │ │ ├── push_left_in.xml
│ │ │ ├── push_left_out.xml
│ │ │ ├── push_right_in.xml
│ │ │ └── push_right_out.xml
│ │ ├── drawable/
│ │ │ ├── counter_minus_button_background.xml
│ │ │ ├── counter_plus_button_background.xml
│ │ │ ├── counter_value_background.xml
│ │ │ ├── ic_access_time.xml
│ │ │ ├── ic_add_circle_24.xml
│ │ │ ├── ic_arrow_back.xml
│ │ │ ├── ic_arrow_drop_down.xml
│ │ │ ├── ic_arrow_up.xml
│ │ │ ├── ic_baseline_delete_72.xml
│ │ │ ├── ic_baseline_delete_outline_24.xml
│ │ │ ├── ic_baseline_download_72.xml
│ │ │ ├── ic_baseline_edit_72.xml
│ │ │ ├── ic_baseline_error_24.xml
│ │ │ ├── ic_baseline_help_outline_24.xml
│ │ │ ├── ic_baseline_inbox_72.xml
│ │ │ ├── ic_baseline_note_72.xml
│ │ │ ├── ic_baseline_notifications_24.xml
│ │ │ ├── ic_baseline_qr_code_scanner_24.xml
│ │ │ ├── ic_baseline_refresh_24.xml
│ │ │ ├── ic_baseline_refresh_error_24.xml
│ │ │ ├── ic_baseline_search_24.xml
│ │ │ ├── ic_baseline_send_72.xml
│ │ │ ├── ic_baseline_sort_24.xml
│ │ │ ├── ic_cancel.xml
│ │ │ ├── ic_chat_bubble_24dp.xml
│ │ │ ├── ic_check_circle_24.xml
│ │ │ ├── ic_download_24.xml
│ │ │ ├── ic_edit_24.xml
│ │ │ ├── ic_folder_open.xml
│ │ │ ├── ic_form_state_blank.xml
│ │ │ ├── ic_form_state_finalized.xml
│ │ │ ├── ic_form_state_saved.xml
│ │ │ ├── ic_form_state_submission_failed.xml
│ │ │ ├── ic_form_state_submitted.xml
│ │ │ ├── ic_information_outline.xml
│ │ │ ├── ic_map.xml
│ │ │ ├── ic_menu_share.xml
│ │ │ ├── ic_navigate_back.xml
│ │ │ ├── ic_navigate_forward.xml
│ │ │ ├── ic_ondemand_video_black_24dp.xml
│ │ │ ├── ic_outline_assignment_accent_24.xml
│ │ │ ├── ic_outline_cloud_accent_24.xml
│ │ │ ├── ic_outline_color_lens_accent_24.xml
│ │ │ ├── ic_outline_delete_accent_24.xml
│ │ │ ├── ic_outline_edit_24.xml
│ │ │ ├── ic_outline_face_accent_24.xml
│ │ │ ├── ic_outline_forum_24.xml
│ │ │ ├── ic_outline_info_small.xml
│ │ │ ├── ic_outline_lock_24.xml
│ │ │ ├── ic_outline_lock_accent_24.xml
│ │ │ ├── ic_outline_lock_open_24.xml
│ │ │ ├── ic_outline_map_accent_24.xml
│ │ │ ├── ic_outline_menu_accent_24.xml
│ │ │ ├── ic_outline_phonelink_setup_accent_24.xml
│ │ │ ├── ic_outline_qr_code_scanner_accent_24.xml
│ │ │ ├── ic_outline_rate_review_24.xml
│ │ │ ├── ic_outline_refresh_accent_24.xml
│ │ │ ├── ic_outline_settings_accent_24.xml
│ │ │ ├── ic_outline_settings_applications_accent_24.xml
│ │ │ ├── ic_outline_share_24.xml
│ │ │ ├── ic_outline_stars_24.xml
│ │ │ ├── ic_outline_vpn_key_accent_24.xml
│ │ │ ├── ic_outline_warning_accent_24.xml
│ │ │ ├── ic_outline_website_24.xml
│ │ │ ├── ic_pause_24dp.xml
│ │ │ ├── ic_person_24dp.xml
│ │ │ ├── ic_play_arrow_24dp.xml
│ │ │ ├── ic_repeat.xml
│ │ │ ├── ic_room_form_state_complete_24dp.xml
│ │ │ ├── ic_room_form_state_complete_48dp.xml
│ │ │ ├── ic_room_form_state_incomplete_24dp.xml
│ │ │ ├── ic_room_form_state_incomplete_48dp.xml
│ │ │ ├── ic_room_form_state_submission_failed_24dp.xml
│ │ │ ├── ic_room_form_state_submission_failed_48dp.xml
│ │ │ ├── ic_room_form_state_submitted_24dp.xml
│ │ │ ├── ic_room_form_state_submitted_48dp.xml
│ │ │ ├── ic_save_menu_24.xml
│ │ │ ├── ic_send_24.xml
│ │ │ ├── ic_sort.xml
│ │ │ ├── ic_sort_by_alpha.xml
│ │ │ ├── ic_sort_by_last_saved.xml
│ │ │ ├── ic_splash_screen_icon.xml
│ │ │ ├── ic_stop_black_24dp.xml
│ │ │ ├── ic_visibility.xml
│ │ │ ├── ic_volume_up_black_24dp.xml
│ │ │ ├── inset_divider_64dp.xml
│ │ │ ├── main_menu_button_background.xml
│ │ │ ├── odk_logo.xml
│ │ │ ├── pill_filled.xml
│ │ │ ├── pill_unfilled.xml
│ │ │ ├── project_icon_background.xml
│ │ │ ├── question_with_error_border.xml
│ │ │ ├── select_item_border.xml
│ │ │ └── start_new_form_button_background.xml
│ │ ├── drawable-ldrtl/
│ │ │ ├── counter_minus_button_background.xml
│ │ │ └── counter_plus_button_background.xml
│ │ ├── layout/
│ │ │ ├── about_item_layout.xml
│ │ │ ├── about_layout.xml
│ │ │ ├── activity_blank_form_list.xml
│ │ │ ├── activity_custom_scanner.xml
│ │ │ ├── activity_preferences_layout.xml
│ │ │ ├── add_repeat_dialog_layout.xml
│ │ │ ├── admin_password_dialog_layout.xml
│ │ │ ├── annotate_widget.xml
│ │ │ ├── audio_controller_layout.xml
│ │ │ ├── audio_player_layout.xml
│ │ │ ├── audio_recording_controller_fragment.xml
│ │ │ ├── audio_video_image_text_label.xml
│ │ │ ├── audio_widget_answer.xml
│ │ │ ├── background_audio_help_fragment_layout.xml
│ │ │ ├── bearing_widget_answer.xml
│ │ │ ├── blank_form_list_item.xml
│ │ │ ├── bottom_sheet.xml
│ │ │ ├── captioned_item.xml
│ │ │ ├── captioned_list_dialog.xml
│ │ │ ├── changes_reason_dialog.xml
│ │ │ ├── checkbox.xml
│ │ │ ├── circular_progress_indicator.xml
│ │ │ ├── counter_widget.xml
│ │ │ ├── current_project_menu_icon.xml
│ │ │ ├── custom_date_picker_dialog.xml
│ │ │ ├── date_time_widget_answer.xml
│ │ │ ├── date_widget_answer.xml
│ │ │ ├── delete_form_layout.xml
│ │ │ ├── delete_project_dialog_layout.xml
│ │ │ ├── draw_widget.xml
│ │ │ ├── ex_audio_widget_answer.xml
│ │ │ ├── ex_image_widget_answer.xml
│ │ │ ├── ex_string_question_type.xml
│ │ │ ├── first_launch_layout.xml
│ │ │ ├── form_chooser_list.xml
│ │ │ ├── form_chooser_list_item.xml
│ │ │ ├── form_chooser_list_item_icon.xml
│ │ │ ├── form_chooser_list_item_map_button.xml
│ │ │ ├── form_chooser_list_item_multiple_choice.xml
│ │ │ ├── form_chooser_list_item_text.xml
│ │ │ ├── form_download_list.xml
│ │ │ ├── form_entry.xml
│ │ │ ├── form_entry_end.xml
│ │ │ ├── form_hierarchy_layout.xml
│ │ │ ├── form_map_activity.xml
│ │ │ ├── fragment_scan.xml
│ │ │ ├── geopoint_question.xml
│ │ │ ├── geoshape_question.xml
│ │ │ ├── geotrace_question.xml
│ │ │ ├── google_drive_deprecation_banner.xml
│ │ │ ├── help_layout.xml
│ │ │ ├── hierarchy_group_item.xml
│ │ │ ├── hierarchy_host_layout.xml
│ │ │ ├── hierarchy_question_item.xml
│ │ │ ├── hierarchy_repeatable_group_instance_item.xml
│ │ │ ├── hierarchy_repeatable_group_item.xml
│ │ │ ├── identify_user_dialog.xml
│ │ │ ├── image_widget.xml
│ │ │ ├── instance_uploader_list.xml
│ │ │ ├── label_widget.xml
│ │ │ ├── launch_intent_button_layout.xml
│ │ │ ├── main_menu.xml
│ │ │ ├── main_menu_activity.xml
│ │ │ ├── main_menu_button.xml
│ │ │ ├── manual_project_creator_dialog_layout.xml
│ │ │ ├── map_button.xml
│ │ │ ├── no_buttons_item_layout.xml
│ │ │ ├── number_picker_dialog.xml
│ │ │ ├── odk_view.xml
│ │ │ ├── osm_widget_answer.xml
│ │ │ ├── password_dialog_layout.xml
│ │ │ ├── permissions_dialog_layout.xml
│ │ │ ├── printer_widget.xml
│ │ │ ├── progress_bar_view.xml
│ │ │ ├── project_icon_view.xml
│ │ │ ├── project_list_item.xml
│ │ │ ├── project_settings_dialog_layout.xml
│ │ │ ├── qr_code_project_creator_dialog_layout.xml
│ │ │ ├── question_error_layout.xml
│ │ │ ├── question_widget.xml
│ │ │ ├── quit_form_dialog_layout.xml
│ │ │ ├── range_picker_widget_answer.xml
│ │ │ ├── range_widget_horizontal.xml
│ │ │ ├── range_widget_vertical.xml
│ │ │ ├── ranking_item.xml
│ │ │ ├── ranking_widget.xml
│ │ │ ├── rating_widget_answer.xml
│ │ │ ├── ready_to_send_banner.xml
│ │ │ ├── reset_dialog_layout.xml
│ │ │ ├── select_image_map_widget_answer.xml
│ │ │ ├── select_list_widget_answer.xml
│ │ │ ├── select_minimal_dialog_layout.xml
│ │ │ ├── select_minimal_widget_answer.xml
│ │ │ ├── select_multi_item.xml
│ │ │ ├── select_one_from_map_widget_answer.xml
│ │ │ ├── select_one_item.xml
│ │ │ ├── server_auth_dialog.xml
│ │ │ ├── show_qrcode_fragment.xml
│ │ │ ├── signature_widget.xml
│ │ │ ├── sort_item_layout.xml
│ │ │ ├── start_new_from_button.xml
│ │ │ ├── tabs_layout.xml
│ │ │ ├── time_widget_answer.xml
│ │ │ ├── transparent_progress_screen.xml
│ │ │ ├── trigger_widget_answer.xml
│ │ │ ├── url_widget_answer.xml
│ │ │ ├── waveform_layout.xml
│ │ │ ├── widget_answer_dialog_layout.xml
│ │ │ └── widget_answer_text.xml
│ │ ├── menu/
│ │ │ ├── blank_form_list_menu.xml
│ │ │ ├── changes_reason_dialog.xml
│ │ │ ├── drafts.xml
│ │ │ ├── form_hierarchy_menu.xml
│ │ │ ├── form_menu.xml
│ │ │ ├── instance_uploader_menu.xml
│ │ │ ├── main_menu.xml
│ │ │ ├── project_preferences_menu.xml
│ │ │ ├── qr_code_scan_menu.xml
│ │ │ ├── saved_form_list_menu.xml
│ │ │ └── select_minimal_dialog_menu.xml
│ │ ├── navigation/
│ │ │ └── form_entry.xml
│ │ ├── raw/
│ │ │ ├── isrgrootx1.pem
│ │ │ └── keep.xml
│ │ ├── values/
│ │ │ ├── arrays.xml
│ │ │ ├── attrs.xml
│ │ │ ├── buttons.xml
│ │ │ ├── colors.xml
│ │ │ ├── date_time_pickers.xml
│ │ │ ├── dimens.xml
│ │ │ ├── form_entry_activity_theme.xml
│ │ │ ├── form_state_colors.xml
│ │ │ ├── leak_canary_config.xml
│ │ │ ├── material_colors.xml
│ │ │ ├── screen_names.xml
│ │ │ ├── settings_theme.xml
│ │ │ ├── shape.xml
│ │ │ ├── styles.xml
│ │ │ ├── theme.xml
│ │ │ └── typography.xml
│ │ ├── values-night/
│ │ │ └── material_colors.xml
│ │ └── xml/
│ │ ├── access_control_preferences.xml
│ │ ├── dev_tools_preferences.xml
│ │ ├── experimental_preferences.xml
│ │ ├── form_entry_access_preferences.xml
│ │ ├── form_management_preferences.xml
│ │ ├── form_metadata_preferences.xml
│ │ ├── identity_preferences.xml
│ │ ├── main_menu_access_preferences.xml
│ │ ├── maps_preferences.xml
│ │ ├── network_security_config.xml
│ │ ├── odk_server_preferences.xml
│ │ ├── project_display_preferences.xml
│ │ ├── project_management_preferences.xml
│ │ ├── project_preferences.xml
│ │ ├── provider_paths.xml
│ │ ├── server_preferences.xml
│ │ ├── user_interface_preferences.xml
│ │ └── user_settings_access_preferences.xml
│ ├── release/
│ │ └── google-services.json
│ └── test/
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ ├── android/
│ │ │ ├── TestSettingsProvider.kt
│ │ │ ├── activities/
│ │ │ │ ├── CrashHandlerActivityTest.kt
│ │ │ │ ├── FirstLaunchActivityTest.kt
│ │ │ │ ├── FormFillingActivityTest.kt
│ │ │ │ └── FormHierarchyFragmentHostActivityTest.kt
│ │ │ ├── application/
│ │ │ │ ├── CollectSettingsChangeHandlerTest.kt
│ │ │ │ ├── RobolectricApplication.java
│ │ │ │ └── initialization/
│ │ │ │ ├── AnalyticsInitializerTest.kt
│ │ │ │ ├── CachedFormsCleanerTest.kt
│ │ │ │ ├── ExistingSettingsMigratorTest.kt
│ │ │ │ ├── GoogleDriveProjectsDeleterTest.kt
│ │ │ │ ├── SavepointsImporterTest.kt
│ │ │ │ ├── ScheduledWorkUpgradeTest.kt
│ │ │ │ └── upgrade/
│ │ │ │ └── BeforeProjectsInstallDetectorTest.kt
│ │ │ ├── audio/
│ │ │ │ ├── AudioButtonTest.java
│ │ │ │ ├── AudioControllerViewTest.java
│ │ │ │ ├── AudioRecordingControllerFragmentTest.java
│ │ │ │ ├── AudioRecordingFormErrorDialogFragmentTest.java
│ │ │ │ └── BackgroundAudioHelpDialogFragmentTest.java
│ │ │ ├── backgroundwork/
│ │ │ │ ├── AutoUpdateTaskSpecTest.kt
│ │ │ │ ├── FormUpdateAndInstanceSubmitSchedulerTest.kt
│ │ │ │ ├── SendFormsTaskSpecTest.kt
│ │ │ │ └── SyncFormsTaskSpecTest.kt
│ │ │ ├── configure/
│ │ │ │ └── qr/
│ │ │ │ ├── QRCodeActivityResultDelegateTest.kt
│ │ │ │ ├── QRCodeMenuProviderTest.kt
│ │ │ │ ├── QRCodeScannerFragmentTest.kt
│ │ │ │ └── QRCodeViewModelTest.kt
│ │ │ ├── database/
│ │ │ │ ├── DatabaseFormsRepositoryTest.java
│ │ │ │ ├── DatabaseInstancesRepositoryTest.kt
│ │ │ │ ├── DatabaseSavepointsRepositoryTest.kt
│ │ │ │ ├── FormDatabaseMigratorTest.java
│ │ │ │ ├── InstanceDatabaseMigratorTest.kt
│ │ │ │ └── entities/
│ │ │ │ └── EntitiesDatabaseMigratorTest.kt
│ │ │ ├── dependencies/
│ │ │ │ ├── BikramSambatTest.java
│ │ │ │ └── PersianCalendarTest.java
│ │ │ ├── dynamicpreload/
│ │ │ │ ├── DynamicPreloadExtraTest.kt
│ │ │ │ ├── DynamicPreloadParseProcessorTest.kt
│ │ │ │ ├── ExternalDataReaderTest.java
│ │ │ │ ├── ExternalDataUseCasesTest.kt
│ │ │ │ └── ExternalDataUtilTest.java
│ │ │ ├── entities/
│ │ │ │ ├── DatabaseEntitiesRepositoryTest.kt
│ │ │ │ ├── EntitiesRepositoryTest.kt
│ │ │ │ ├── InMemEntitiesRepositoryTest.kt
│ │ │ │ └── support/
│ │ │ │ └── EntitySameAsMatcher.kt
│ │ │ ├── external/
│ │ │ │ ├── FormUriActivityTest.kt
│ │ │ │ ├── FormsProviderTest.java
│ │ │ │ └── InstanceProviderTest.java
│ │ │ ├── fakes/
│ │ │ │ └── FakePermissionsProvider.kt
│ │ │ ├── formentry/
│ │ │ │ ├── AppStateFormSessionRepositoryTest.kt
│ │ │ │ ├── AudioVideoImageTextLabelTest.java
│ │ │ │ ├── AudioVideoImageTextLabelVisibilityTest.kt
│ │ │ │ ├── BackgroundAudioPermissionDialogFragmentTest.java
│ │ │ │ ├── BackgroundAudioViewModelTest.java
│ │ │ │ ├── FormEndTest.kt
│ │ │ │ ├── FormEndViewModelTest.kt
│ │ │ │ ├── FormEntryMenuProviderTest.kt
│ │ │ │ ├── FormEntryUseCasesTest.kt
│ │ │ │ ├── FormEntryViewModelTest.java
│ │ │ │ ├── FormLoadingDialogFragmentTest.java
│ │ │ │ ├── FormSessionRepositoryTest.kt
│ │ │ │ ├── InMemoryFormSessionRepositoryTest.kt
│ │ │ │ ├── PrinterWidgetViewModelTest.kt
│ │ │ │ ├── QuitFormDialogTest.kt
│ │ │ │ ├── RecordingHandlerTest.java
│ │ │ │ ├── RefreshFormListDialogFragmentTest.java
│ │ │ │ ├── SaveFormProgressDialogFragmentTest.java
│ │ │ │ ├── audit/
│ │ │ │ │ ├── AsyncTaskAuditEventWriterTest.java
│ │ │ │ │ ├── AuditConfigTest.java
│ │ │ │ │ ├── AuditEventCSVLineTest.java
│ │ │ │ │ ├── AuditEventLoggerTest.java
│ │ │ │ │ ├── AuditEventTest.java
│ │ │ │ │ ├── FormSaveViewModelTest.java
│ │ │ │ │ └── IdentityPromptViewModelTest.java
│ │ │ │ ├── backgroundlocation/
│ │ │ │ │ └── BackgroundLocationManagerTest.java
│ │ │ │ ├── repeats/
│ │ │ │ │ └── DeleteRepeatDialogFragmentTest.java
│ │ │ │ └── support/
│ │ │ │ └── InMemFormSessionRepository.kt
│ │ │ ├── formhierarchy/
│ │ │ │ ├── HierarchyListItemViewTest.kt
│ │ │ │ └── QuestionAnswerProcessorTest.kt
│ │ │ ├── formlists/
│ │ │ │ ├── DeleteBlankFormFragmentTest.kt
│ │ │ │ ├── blankformlist/
│ │ │ │ │ ├── BlankFormListItemTest.kt
│ │ │ │ │ ├── BlankFormListItemViewTest.kt
│ │ │ │ │ ├── BlankFormListMenuProviderTest.kt
│ │ │ │ │ └── BlankFormListViewModelTest.kt
│ │ │ │ └── savedformlist/
│ │ │ │ ├── DeleteSavedFormFragmentTest.kt
│ │ │ │ ├── SavedFormListListMenuProviderTest.kt
│ │ │ │ └── SavedFormListViewModelTest.kt
│ │ │ ├── formmanagement/
│ │ │ │ ├── DownloadMediaFilesServerFormUseCasesTest.kt
│ │ │ │ ├── DownloadUpdatesServerFormUseCasesTest.kt
│ │ │ │ ├── FetchFormDetailsServerFormUseCasesTest.kt
│ │ │ │ ├── FilterFormsToAddTest.kt
│ │ │ │ ├── FormFillingIntentFactoryTest.kt
│ │ │ │ ├── FormSourceExceptionMapperTest.kt
│ │ │ │ ├── FormsDataServiceTest.kt
│ │ │ │ ├── LocalFormUseCasesTest.java
│ │ │ │ ├── ServerFormsSynchronizerTest.java
│ │ │ │ ├── ShouldAddFormFileTest.java
│ │ │ │ ├── download/
│ │ │ │ │ ├── FormDownloadExceptionMapperTest.kt
│ │ │ │ │ └── ServerFormDownloaderTest.java
│ │ │ │ ├── drafts/
│ │ │ │ │ └── DraftsMenuProviderTest.kt
│ │ │ │ ├── formmap/
│ │ │ │ │ └── FormMapViewModelTest.kt
│ │ │ │ └── metadata/
│ │ │ │ └── FormMetadataParserTest.kt
│ │ │ ├── fragments/
│ │ │ │ ├── dialogs/
│ │ │ │ │ ├── FormsDownloadResultDialogTest.kt
│ │ │ │ │ ├── RangePickerDialogFragmentTest.kt
│ │ │ │ │ ├── SelectMinimalDialogTest.java
│ │ │ │ │ ├── SelectMultiMinimalDialogTest.java
│ │ │ │ │ └── SelectOneMinimalDialogTest.java
│ │ │ │ └── support/
│ │ │ │ └── DialogFragmentHelpers.java
│ │ │ ├── geo/
│ │ │ │ └── MapFragmentFactoryImplTest.kt
│ │ │ ├── instancemanagement/
│ │ │ │ ├── InstanceDeleterTest.kt
│ │ │ │ ├── InstanceExtKtTest.kt
│ │ │ │ ├── InstanceListItemViewTest.kt
│ │ │ │ ├── InstancesDataServiceTest.kt
│ │ │ │ ├── LocalInstancesUseCasesTest.kt
│ │ │ │ ├── autosend/
│ │ │ │ │ ├── AutoSendSettingsProviderTest.kt
│ │ │ │ │ ├── FormExtTest.kt
│ │ │ │ │ └── InstanceAutoSendFetcherTest.kt
│ │ │ │ └── send/
│ │ │ │ ├── ReadyToSendBannerTest.kt
│ │ │ │ └── ReadyToSendViewModelTest.kt
│ │ │ ├── javarosawrapper/
│ │ │ │ ├── FakeFormController.java
│ │ │ │ └── FormControllerTest.java
│ │ │ ├── location/
│ │ │ │ └── client/
│ │ │ │ ├── FakeLocationClient.java
│ │ │ │ └── MaxAccuracyWithinTimeoutLocationClientWrapperTest.java
│ │ │ ├── mainmenu/
│ │ │ │ ├── CurrentProjectViewModelTest.kt
│ │ │ │ ├── MainMenuActivityTest.kt
│ │ │ │ ├── MainMenuButtonTest.kt
│ │ │ │ ├── MainMenuViewModelTest.kt
│ │ │ │ ├── PermissionsDialogFragmentTest.kt
│ │ │ │ └── RequestPermissionsViewModelTest.kt
│ │ │ ├── notifications/
│ │ │ │ └── NotificationManagerNotifierTest.kt
│ │ │ ├── preferences/
│ │ │ │ ├── AppConfigurationGeneratorTest.kt
│ │ │ │ ├── ProjectPreferencesViewModelTest.kt
│ │ │ │ ├── ServerPreferencesAdderTest.java
│ │ │ │ ├── dialogs/
│ │ │ │ │ ├── AdminPasswordDialogFragmentTest.kt
│ │ │ │ │ ├── ChangeAdminPasswordDialogTest.kt
│ │ │ │ │ ├── DeleteProjectDialogTest.kt
│ │ │ │ │ ├── ResetProgressDialogTest.kt
│ │ │ │ │ └── ServerAuthDialogFragmentTest.java
│ │ │ │ ├── screens/
│ │ │ │ │ ├── FormEntryAccessPreferencesFragmentTest.kt
│ │ │ │ │ ├── FormManagementPreferencesFragmentTest.kt
│ │ │ │ │ ├── FormMetadataPreferencesFragmentTest.kt
│ │ │ │ │ ├── IdentityPreferencesFragmentTest.kt
│ │ │ │ │ ├── MainMenuAccessPreferencesTest.kt
│ │ │ │ │ ├── MapsPreferencesFragmentTest.kt
│ │ │ │ │ ├── ProjectDisplayPreferencesFragmentTest.kt
│ │ │ │ │ ├── ProjectPreferencesFragmentTest.kt
│ │ │ │ │ └── UserInterfacePreferencesFragmentTest.kt
│ │ │ │ └── source/
│ │ │ │ ├── SettingsStoreTest.kt
│ │ │ │ ├── SharedPreferencesSettingsProviderTest.kt
│ │ │ │ └── SharedPreferencesSettingsTest.kt
│ │ │ ├── projects/
│ │ │ │ ├── ExistingProjectMigratorTest.kt
│ │ │ │ ├── ManualProjectCreatorDialogTest.kt
│ │ │ │ ├── ProjectCreatorImplTest.kt
│ │ │ │ ├── ProjectDeleterTest.kt
│ │ │ │ ├── ProjectIconViewTest.kt
│ │ │ │ ├── ProjectListItemViewTest.kt
│ │ │ │ ├── ProjectResetterTest.kt
│ │ │ │ ├── ProjectSettingsDialogTest.kt
│ │ │ │ ├── ProjectsDataServiceTest.kt
│ │ │ │ ├── QrCodeProjectCreatorDialogTest.kt
│ │ │ │ └── SettingsConnectionMatcherImplTest.kt
│ │ │ ├── savepoints/
│ │ │ │ └── SavepointUseCasesTest.kt
│ │ │ ├── storage/
│ │ │ │ └── StoragePathProviderTest.kt
│ │ │ ├── support/
│ │ │ │ ├── CollectHelpers.java
│ │ │ │ ├── Matchers.kt
│ │ │ │ ├── MockFormEntryPromptBuilder.java
│ │ │ │ ├── SwipableParentActivity.kt
│ │ │ │ └── WidgetTestActivity.kt
│ │ │ ├── tasks/
│ │ │ │ └── SaveFormIndexTaskTest.java
│ │ │ ├── utilities/
│ │ │ │ ├── AdminPasswordProviderTest.java
│ │ │ │ ├── AppearancesTest.kt
│ │ │ │ ├── ArrayUtilsTest.java
│ │ │ │ ├── CSVUtilsTest.java
│ │ │ │ ├── ChangeLockProviderTest.kt
│ │ │ │ ├── ExternalAppIntentProviderTest.kt
│ │ │ │ ├── ExternalAppUtilsTest.java
│ │ │ │ ├── FileUtilsTest.java
│ │ │ │ ├── FormNameUtilsTest.java
│ │ │ │ ├── FormsDownloadResultInterpreterTest.kt
│ │ │ │ ├── FormsRepositoryProviderTest.kt
│ │ │ │ ├── FormsUploadResultInterpreterTest.kt
│ │ │ │ ├── HtmlUtilsTest.kt
│ │ │ │ ├── ImageCompressionControllerTest.kt
│ │ │ │ ├── InstanceAutoDeleteCheckerTest.kt
│ │ │ │ ├── InstanceUploaderUtilsTest.java
│ │ │ │ ├── InstancesRepositoryProviderTest.kt
│ │ │ │ ├── MediaUtilsTest.kt
│ │ │ │ ├── MyanmarDateUtilsTest.java
│ │ │ │ ├── QuestionFontSizeUtilsTest.java
│ │ │ │ ├── StubFormController.kt
│ │ │ │ └── WebCredentialsUtilsTest.java
│ │ │ ├── version/
│ │ │ │ └── VersionInformationTest.java
│ │ │ ├── views/
│ │ │ │ ├── ChoicesRecyclerViewTest.java
│ │ │ │ ├── TrackingTouchSliderTest.java
│ │ │ │ └── helpers/
│ │ │ │ └── PromptAutoplayerTest.java
│ │ │ └── widgets/
│ │ │ ├── AnnotateWidgetTest.java
│ │ │ ├── ArbitraryFileWidgetTest.kt
│ │ │ ├── AudioWidgetTest.java
│ │ │ ├── BarcodeWidgetTest.kt
│ │ │ ├── BearingWidgetTest.java
│ │ │ ├── CounterWidgetTest.kt
│ │ │ ├── DecimalWidgetTest.java
│ │ │ ├── DrawWidgetTest.java
│ │ │ ├── ExArbitraryFileWidgetTest.kt
│ │ │ ├── ExAudioWidgetTest.java
│ │ │ ├── ExDecimalWidgetTest.java
│ │ │ ├── ExImageWidgetTest.java
│ │ │ ├── ExIntegerWidgetTest.java
│ │ │ ├── ExStringWidgetTest.kt
│ │ │ ├── ExVideoWidgetTest.kt
│ │ │ ├── GeoPointMapWidgetTest.java
│ │ │ ├── GeoPointWidgetTest.java
│ │ │ ├── GeoShapeWidgetTest.java
│ │ │ ├── GeoTraceWidgetTest.java
│ │ │ ├── ImageWidgetTest.java
│ │ │ ├── IntegerWidgetTest.java
│ │ │ ├── OSMWidgetTest.java
│ │ │ ├── PrinterWidgetTest.kt
│ │ │ ├── QuestionWidgetTest.java
│ │ │ ├── RatingWidgetTest.java
│ │ │ ├── SignatureWidgetTest.java
│ │ │ ├── StringNumberWidgetTest.java
│ │ │ ├── StringWidgetTest.kt
│ │ │ ├── TriggerWidgetTest.java
│ │ │ ├── UrlWidgetTest.java
│ │ │ ├── VideoWidgetTest.kt
│ │ │ ├── WidgetFactoryTest.kt
│ │ │ ├── base/
│ │ │ │ ├── BinaryWidgetTest.java
│ │ │ │ ├── FileWidgetTest.java
│ │ │ │ ├── GeneralExStringWidgetTest.java
│ │ │ │ ├── GeneralSelectMultiWidgetTest.java
│ │ │ │ ├── GeneralSelectOneWidgetTest.java
│ │ │ │ ├── GeneralStringWidgetTest.java
│ │ │ │ ├── QuestionWidgetTest.java
│ │ │ │ ├── SelectWidgetTest.java
│ │ │ │ └── WidgetTest.java
│ │ │ ├── datetime/
│ │ │ │ ├── DateTimeUtilsTest.java
│ │ │ │ ├── DateTimeWidgetTest.java
│ │ │ │ ├── DateTimeWidgetUtilsTest.java
│ │ │ │ ├── DateWidgetTest.java
│ │ │ │ ├── DaylightSavingTest.java
│ │ │ │ ├── TimeWidgetTest.java
│ │ │ │ └── pickers/
│ │ │ │ ├── BikramSambatDatePickerDialogTest.java
│ │ │ │ ├── BuddhistDatePickerDialogTest.kt
│ │ │ │ ├── CopticDatePickerDialogTest.java
│ │ │ │ ├── EthiopianDatePickerDialogTest.java
│ │ │ │ ├── IslamicDatePickerDialogTest.java
│ │ │ │ ├── MyanmarDatePickerDialogTest.java
│ │ │ │ └── PersianDatePickerDialogTest.java
│ │ │ ├── items/
│ │ │ │ ├── LikertWidgetTest.java
│ │ │ │ ├── ListMultiWidgetTest.java
│ │ │ │ ├── ListWidgetTest.java
│ │ │ │ ├── RankingWidgetTest.java
│ │ │ │ ├── SelectChoicesMapDataTest.kt
│ │ │ │ ├── SelectImageMapWidgetTest.java
│ │ │ │ ├── SelectMultiImageMapWidgetTest.java
│ │ │ │ ├── SelectMultiMinimalWidgetTest.java
│ │ │ │ ├── SelectMultiWidgetTest.java
│ │ │ │ ├── SelectOneFromMapDialogFragmentTest.kt
│ │ │ │ ├── SelectOneFromMapWidgetTest.kt
│ │ │ │ ├── SelectOneImageMapWidgetTest.java
│ │ │ │ ├── SelectOneMinimalWidgetTest.java
│ │ │ │ └── SelectOneWidgetTest.java
│ │ │ ├── range/
│ │ │ │ ├── RangeDecimalWidgetTest.java
│ │ │ │ ├── RangeIntegerWidgetTest.java
│ │ │ │ ├── RangePickerWidgetTest.kt
│ │ │ │ └── RangePickerWidgetUtilsTest.kt
│ │ │ ├── support/
│ │ │ │ ├── FakeQuestionMediaManager.java
│ │ │ │ ├── FakeWaitingForDataRegistry.java
│ │ │ │ ├── FormElementFixtures.kt
│ │ │ │ ├── FormEntryPromptSelectChoiceLoader.kt
│ │ │ │ ├── GeoWidgetHelpers.java
│ │ │ │ ├── NoOpMapFragment.kt
│ │ │ │ ├── QuestionWidgetHelpers.java
│ │ │ │ └── SynchronousImageLoader.kt
│ │ │ ├── utilities/
│ │ │ │ ├── ActivityGeoDataRequesterTest.java
│ │ │ │ ├── AudioRecorderRecordingStatusHandlerTest.java
│ │ │ │ ├── ExternalAppRecordingRequesterTest.kt
│ │ │ │ ├── FileRequesterImplTest.kt
│ │ │ │ ├── GeoPolyDialogFragmentTest.kt
│ │ │ │ ├── GeoWidgetUtilsTest.kt
│ │ │ │ ├── GetContentAudioFileRequesterTest.kt
│ │ │ │ ├── InternalRecordingRequesterTest.java
│ │ │ │ ├── RangeWidgetUtilsTest.java
│ │ │ │ ├── RecordingRequesterProviderTest.java
│ │ │ │ ├── StringRequesterImplTest.kt
│ │ │ │ └── StringWidgetUtilsTest.java
│ │ │ ├── viewmodels/
│ │ │ │ ├── DateTimeViewModelTest.java
│ │ │ │ └── QuestionViewModelTest.kt
│ │ │ └── warnings/
│ │ │ ├── SpacesInUnderlyingValuesTest.java
│ │ │ └── SpacesInUnderlyingValuesWarningTest.java
│ │ └── geo/
│ │ └── javarosa/
│ │ └── IntersectsFunctionHandlerTest.kt
│ └── resources/
│ ├── forms/
│ │ └── simple-search-external-csv.xml
│ ├── media/
│ │ └── simple-search-external-csv-fruits.csv
│ └── robolectric.properties
├── config/
│ ├── checkstyle.xml
│ ├── lint.xml
│ ├── pmd-ruleset.xml
│ └── quality.gradle
├── crash-handler/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── crashhandler/
│ │ │ ├── CrashHandler.kt
│ │ │ ├── CrashView.kt
│ │ │ └── MockCrashView.kt
│ │ └── res/
│ │ └── layout/
│ │ └── crash_layout.xml
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── crashhandler/
│ └── CrashHandlerTest.kt
├── create-release.sh
├── db/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── db/
│ │ └── sqlite/
│ │ ├── AltDatabasePathContext.kt
│ │ ├── CursorExt.kt
│ │ ├── CustomSQLiteQueryBuilder.java
│ │ ├── CustomSQLiteQueryExecutor.java
│ │ ├── DatabaseConnection.kt
│ │ ├── DatabaseMigrator.java
│ │ ├── MigrationListDatabaseMigrator.kt
│ │ ├── RowNumbers.kt
│ │ ├── SQLiteColumns.kt
│ │ ├── SQLiteDatabaseExt.kt
│ │ ├── SQLiteUtils.java
│ │ ├── SqlQuery.kt
│ │ └── SynchronizedDatabaseConnection.kt
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── db/
│ └── sqlite/
│ ├── CustomSQLiteQueryBuilderTest.java
│ ├── DatabaseConnectionTest.kt
│ ├── RowNumbersTest.kt
│ ├── SQLiteUtilsTest.java
│ ├── SqlQueryTest.kt
│ ├── SqliteDatabaseExtTest.kt
│ └── support/
│ └── NoopMigrator.kt
├── debug.keystore
├── docs/
│ ├── ANALYTICS-QUESTIONS.md
│ ├── CODE-GUIDELINES.md
│ ├── CONTRIBUTING.md
│ ├── STATE.md
│ ├── TEST-GUIDELINES.md
│ ├── WIDGETS.md
│ └── WINDOWS-DEV-SETUP.md
├── download-robolectric-deps.sh
├── draw/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── draw/
│ │ │ ├── DaggerSetup.kt
│ │ │ ├── DrawActivity.java
│ │ │ ├── DrawView.kt
│ │ │ ├── DrawViewModel.kt
│ │ │ ├── PenColorPickerDialog.kt
│ │ │ ├── PenColorPickerViewModel.kt
│ │ │ ├── QuitDrawingDialog.kt
│ │ │ └── RobolectricApplication.kt
│ │ └── res/
│ │ └── layout/
│ │ ├── draw_layout.xml
│ │ └── quit_drawing_dialog_layout.xml
│ └── test/
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── draw/
│ │ ├── DrawActivityTest.kt
│ │ ├── PenColorPickerDialogTest.kt
│ │ └── PenColorPickerViewModelTest.kt
│ └── resources/
│ └── robolectric.properties
├── entities/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── entities/
│ │ │ ├── DaggerSetup.kt
│ │ │ ├── LocalEntityUseCases.kt
│ │ │ ├── browser/
│ │ │ │ ├── EntitiesFragment.kt
│ │ │ │ ├── EntitiesViewModel.kt
│ │ │ │ ├── EntityBrowserActivity.kt
│ │ │ │ ├── EntityItem.kt
│ │ │ │ └── EntityListsFragment.kt
│ │ │ ├── javarosa/
│ │ │ │ ├── filter/
│ │ │ │ │ ├── LocalEntitiesFilterStrategy.kt
│ │ │ │ │ └── PullDataFunctionHandler.kt
│ │ │ │ ├── finalization/
│ │ │ │ │ ├── EntitiesExtra.kt
│ │ │ │ │ ├── EntityFormFinalizationProcessor.kt
│ │ │ │ │ ├── FormEntity.kt
│ │ │ │ │ └── InvalidEntity.kt
│ │ │ │ ├── intance/
│ │ │ │ │ ├── LocalEntitiesExternalInstanceParserFactory.kt
│ │ │ │ │ ├── LocalEntitiesInstanceAdapter.kt
│ │ │ │ │ └── LocalEntitiesInstanceProvider.kt
│ │ │ │ ├── parse/
│ │ │ │ │ ├── EntityFormExtra.kt
│ │ │ │ │ ├── EntityFormParseProcessor.kt
│ │ │ │ │ ├── EntitySchema.kt
│ │ │ │ │ ├── EntityXFormParserFactory.kt
│ │ │ │ │ ├── SaveTo.kt
│ │ │ │ │ ├── StringExt.kt
│ │ │ │ │ └── XPathExpressionExt.kt
│ │ │ │ └── spec/
│ │ │ │ ├── EntityAction.kt
│ │ │ │ ├── EntityFormParser.kt
│ │ │ │ ├── FormEntityElement.kt
│ │ │ │ └── UnrecognizedEntityVersionException.kt
│ │ │ ├── server/
│ │ │ │ └── EntitySource.kt
│ │ │ └── storage/
│ │ │ ├── EntitiesRepository.kt
│ │ │ ├── Entity.kt
│ │ │ ├── EntityList.kt
│ │ │ ├── InMemEntitiesRepository.kt
│ │ │ └── QueryException.kt
│ │ └── res/
│ │ ├── layout/
│ │ │ ├── entities_layout.xml
│ │ │ ├── entity_list_item_layout.xml
│ │ │ └── list_layout.xml
│ │ └── navigation/
│ │ └── entities_nav.xml
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── entities/
│ ├── LocalEntityUseCasesTest.kt
│ ├── browser/
│ │ └── EntityItemTest.kt
│ └── javarosa/
│ ├── EntitiesTest.kt
│ ├── EntityFormFinalizationProcessorTest.kt
│ ├── EntityFormParseProcessorTest.kt
│ ├── EntityFormParserTest.kt
│ ├── LocalEntitiesInstanceProviderTest.kt
│ ├── filter/
│ │ ├── LocalEntitiesFilterStrategyTest.kt
│ │ └── PullDataFunctionHandlerTest.kt
│ ├── parse/
│ │ ├── StringExtTest.kt
│ │ └── XPathExpressionExtTest.kt
│ └── support/
│ └── EntityXFormsElement.kt
├── errors/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── errors/
│ │ │ ├── ErrorActivity.kt
│ │ │ ├── ErrorAdapter.kt
│ │ │ └── ErrorItem.kt
│ │ └── res/
│ │ └── layout/
│ │ ├── activity_error.xml
│ │ └── error_item.xml
│ └── test/
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── errors/
│ │ └── ErrorActivityTest.kt
│ └── resources/
│ └── robolectric.properties
├── external-app/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── externalapp/
│ │ └── ExternalAppUtils.kt
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── externalapp/
│ └── ExternalAppUtilsTest.kt
├── forms/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── forms/
│ │ ├── Form.java
│ │ ├── FormListItem.kt
│ │ ├── FormSource.java
│ │ ├── FormSourceException.kt
│ │ ├── FormsRepository.java
│ │ ├── ManifestFile.kt
│ │ ├── MediaFile.kt
│ │ ├── instances/
│ │ │ ├── Instance.java
│ │ │ └── InstancesRepository.java
│ │ └── savepoints/
│ │ ├── Savepoint.kt
│ │ └── SavepointsRepository.kt
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── forms/
│ └── instances/
│ └── InstanceTest.kt
├── forms-test/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── formstest/
│ │ ├── FormFixtures.kt
│ │ ├── FormUtils.kt
│ │ ├── FormsRepositoryTest.java
│ │ ├── InMemFormsRepository.java
│ │ ├── InMemInstancesRepository.java
│ │ ├── InMemSavepointsRepository.kt
│ │ ├── InstanceFixtures.kt
│ │ ├── InstanceUtils.kt
│ │ ├── InstancesRepositoryTest.kt
│ │ └── SavepointsRepositoryTest.kt
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── formstest/
│ ├── InMemFormsRepositoryTest.java
│ ├── InMemInstancesRepositoryTest.kt
│ └── InMemSavepointsRepositoryTest.kt
├── fragments-test/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── fragmentstest/
│ └── FragmentScenarioLauncherRule.kt
├── geo/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── geo/
│ │ │ ├── Constants.kt
│ │ │ ├── DaggerSetup.kt
│ │ │ ├── GeoActivityUtils.kt
│ │ │ ├── GeoUtils.kt
│ │ │ ├── analytics/
│ │ │ │ └── AnalyticsEvents.kt
│ │ │ ├── geopoint/
│ │ │ │ ├── AccuracyProgressView.kt
│ │ │ │ ├── AccuracyStatusView.kt
│ │ │ │ ├── GeoPointActivity.kt
│ │ │ │ ├── GeoPointDialogFragment.kt
│ │ │ │ ├── GeoPointMapActivity.java
│ │ │ │ ├── GeoPointViewModel.kt
│ │ │ │ └── LocationAccuracy.kt
│ │ │ ├── geopoly/
│ │ │ │ ├── GeoPolyFragment.kt
│ │ │ │ ├── GeoPolySettingsDialogFragment.java
│ │ │ │ ├── GeoPolyUtils.kt
│ │ │ │ ├── GeoPolyViewModel.kt
│ │ │ │ └── InfoDialog.kt
│ │ │ ├── javarosa/
│ │ │ │ └── IntersectsFunctionHandler.kt
│ │ │ └── selection/
│ │ │ ├── MappableSelectItem.kt
│ │ │ ├── SelectionMapFragment.kt
│ │ │ └── SelectionSummarySheet.kt
│ │ └── res/
│ │ ├── color/
│ │ │ └── fab_surface_background_color_less_transparent_disabled.xml
│ │ ├── drawable/
│ │ │ ├── ic_add_location.xml
│ │ │ ├── ic_backspace.xml
│ │ │ ├── ic_crop_frame.xml
│ │ │ ├── ic_distance.xml
│ │ │ ├── ic_info.xml
│ │ │ ├── ic_layers.xml
│ │ │ ├── ic_my_location.xml
│ │ │ ├── ic_note_add.xml
│ │ │ ├── ic_pause_36.xml
│ │ │ └── property_divider.xml
│ │ ├── layout/
│ │ │ ├── accuracy_progress_layout.xml
│ │ │ ├── accuracy_status_layout.xml
│ │ │ ├── geopoint_dialog.xml
│ │ │ ├── geopoint_layout.xml
│ │ │ ├── geopoly_dialog.xml
│ │ │ ├── geopoly_layout.xml
│ │ │ ├── property.xml
│ │ │ ├── selection_map_layout.xml
│ │ │ ├── selection_summary_sheet_layout.xml
│ │ │ └── simple_spinner_dropdown_item.xml
│ │ ├── layout-land/
│ │ │ ├── geopoint_layout.xml
│ │ │ └── geopoly_layout.xml
│ │ └── values/
│ │ ├── attrs.xml
│ │ ├── fab_surface.xml
│ │ └── force_light_surface_overlay.xml
│ └── test/
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── geo/
│ │ ├── GeoUtilsTest.kt
│ │ ├── geopoint/
│ │ │ ├── AccuracyProgressViewTest.kt
│ │ │ ├── AccuracyStatusViewTest.kt
│ │ │ ├── GeoPointActivityTest.kt
│ │ │ ├── GeoPointDialogFragmentTest.kt
│ │ │ ├── GeoPointMapActivityTest.java
│ │ │ └── LocationTrackerGeoPointViewModelTest.kt
│ │ ├── geopoly/
│ │ │ ├── GeoPolyFragmentTest.kt
│ │ │ ├── GeoPolySettingsDialogFragmentTest.java
│ │ │ ├── GeoPolyUtilsTest.kt
│ │ │ ├── GeoPolyViewModelTest.kt
│ │ │ └── InfoContentTest.kt
│ │ ├── selection/
│ │ │ ├── SelectionMapFragmentTest.kt
│ │ │ └── SelectionSummarySheetTest.kt
│ │ └── support/
│ │ ├── AccuracyStatusViewMatcher.kt
│ │ ├── FakeLocationTracker.kt
│ │ ├── FakeMapFragment.kt
│ │ ├── Fixtures.kt
│ │ └── RobolectricApplication.kt
│ └── resources/
│ └── robolectric.properties
├── google-maps/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── googlemaps/
│ │ ├── BitmapDescriptorCache.kt
│ │ ├── DaggerSetup.kt
│ │ ├── GoogleMapConfigurator.java
│ │ ├── GoogleMapFragment.java
│ │ ├── GoogleMapsMapBoxOfflineTileProvider.java
│ │ ├── MapPointExt.kt
│ │ ├── circles/
│ │ │ └── CircleFeature.kt
│ │ └── scaleview/
│ │ ├── Drawer.java
│ │ ├── MapScaleModel.java
│ │ ├── MapScaleView.java
│ │ ├── Scale.java
│ │ ├── Scales.java
│ │ └── ViewConfig.java
│ └── res/
│ ├── layout/
│ │ └── map_layout.xml
│ └── values/
│ └── attrs.xml
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── icons/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── res/
│ └── drawable/
│ ├── ic_add_white_24.xml
│ ├── ic_baseline_add_24.xml
│ ├── ic_baseline_barcode_scanner_white_24.xml
│ ├── ic_baseline_calendar_today_white_24.xml
│ ├── ic_baseline_check_24.xml
│ ├── ic_baseline_collapse_24.xml
│ ├── ic_baseline_done_all_24.xml
│ ├── ic_baseline_draw_white_24.xml
│ ├── ic_baseline_expand_24.xml
│ ├── ic_baseline_explore_white_24.xml
│ ├── ic_baseline_format_list_bulleted_white_24.xml
│ ├── ic_baseline_format_list_numbered_white_24.xml
│ ├── ic_baseline_language_24.xml
│ ├── ic_baseline_layers_24.xml
│ ├── ic_baseline_library_music_white_24.xml
│ ├── ic_baseline_list_24.xml
│ ├── ic_baseline_location_off_24.xml
│ ├── ic_baseline_location_on_24.xml
│ ├── ic_baseline_location_on_white_24.xml
│ ├── ic_baseline_markup_white_24.xml
│ ├── ic_baseline_mic_24.xml
│ ├── ic_baseline_mic_off_24.xml
│ ├── ic_baseline_mic_white_24.xml
│ ├── ic_baseline_my_location_white_24.xml
│ ├── ic_baseline_open_in_new_white_24.xml
│ ├── ic_baseline_photo_camera_white_24.xml
│ ├── ic_baseline_photo_library_white_24.xml
│ ├── ic_baseline_print_white_24.xml
│ ├── ic_baseline_qr_code_2_add_24.xml
│ ├── ic_baseline_remove_24.xml
│ ├── ic_baseline_rule_24.xml
│ ├── ic_baseline_settings_24.xml
│ ├── ic_baseline_signature_white_24.xml
│ ├── ic_baseline_time_filled_white_24.xml
│ ├── ic_baseline_visibility_24.xml
│ ├── ic_baseline_warning_24.xml
│ ├── ic_baseline_wifi_off_24.xml
│ ├── ic_clear_white.xml
│ ├── ic_close.xml
│ ├── ic_color_lens_white.xml
│ ├── ic_delete.xml
│ ├── ic_delete_24.xml
│ ├── ic_edit.xml
│ ├── ic_map_marker_big.xml
│ ├── ic_map_marker_small.xml
│ ├── ic_map_marker_with_hole_big.xml
│ ├── ic_map_marker_with_hole_small.xml
│ ├── ic_map_point.xml
│ ├── ic_notification_small.xml
│ ├── ic_outline_info_24.xml
│ ├── ic_outline_polygon_white_24.xml
│ ├── ic_outline_polyline_white_24.xml
│ ├── ic_save.xml
│ └── ic_save_white.xml
├── image-loader/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── imageloader/
│ ├── GlideImageLoader.kt
│ └── svg/
│ ├── SvgDecoder.kt
│ ├── SvgDrawableTranscoder.kt
│ ├── SvgModule.kt
│ └── SvgSoftwareLayerSetter.kt
├── lists/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── lists/
│ │ │ ├── EmptyListView.kt
│ │ │ ├── RecyclerViewUtils.kt
│ │ │ └── selects/
│ │ │ ├── MultiSelectAdapter.kt
│ │ │ ├── MultiSelectControlsFragment.kt
│ │ │ ├── MultiSelectListFragment.kt
│ │ │ ├── MultiSelectViewModel.kt
│ │ │ ├── SelectItem.kt
│ │ │ └── SingleSelectViewModel.kt
│ │ └── res/
│ │ ├── layout/
│ │ │ ├── empty_list_view.xml
│ │ │ ├── multi_select_controls_layout.xml
│ │ │ └── multi_select_list.xml
│ │ └── values/
│ │ └── attrs.xml
│ └── test/
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── lists/
│ │ ├── EmptyListViewTest.kt
│ │ ├── RobolectricApplication.kt
│ │ └── selects/
│ │ ├── MultiSelectAdapterTest.kt
│ │ ├── MultiSelectControlsFragmentTest.kt
│ │ ├── MultiSelectListFragmentTest.kt
│ │ ├── MultiSelectViewModelTest.kt
│ │ ├── SingleSelectViewModelTest.kt
│ │ └── support/
│ │ └── TextAndCheckboxViewHolder.kt
│ └── resources/
│ └── robolectric.properties
├── location/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── location/
│ │ ├── AndroidLocationClient.java
│ │ ├── BaseLocationClient.kt
│ │ ├── DaggerSetup.kt
│ │ ├── GoogleFusedLocationClient.kt
│ │ ├── Location.kt
│ │ ├── LocationClient.java
│ │ ├── LocationClientProvider.kt
│ │ ├── LocationUtils.kt
│ │ ├── satellites/
│ │ │ ├── GpsStatusSatelliteInfoClient.kt
│ │ │ └── SatelliteInfoClient.kt
│ │ └── tracker/
│ │ ├── ForegroundServiceLocationTracker.kt
│ │ └── LocationTracker.kt
│ └── test/
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── location/
│ │ ├── AndroidLocationClientTest.java
│ │ ├── GoogleFusedLocationClientTest.kt
│ │ ├── LocationClientProviderTest.kt
│ │ ├── LocationUtilsTest.kt
│ │ ├── RobolectricApplication.kt
│ │ ├── TestClientListener.java
│ │ ├── TestLocationListener.java
│ │ └── tracker/
│ │ ├── ForegroundServiceLocationTrackerTest.kt
│ │ ├── LocationTrackerServiceTest.kt
│ │ └── LocationTrackerTest.kt
│ └── resources/
│ └── robolectric.properties
├── mapbox/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── mapbox/
│ │ ├── DynamicPolyLineFeature.kt
│ │ ├── DynamicPolygonFeature.kt
│ │ ├── LineFeature.kt
│ │ ├── MapBoxInitializationFragment.kt
│ │ ├── MapFeature.kt
│ │ ├── MapUtils.kt
│ │ ├── MapboxMapConfigurator.java
│ │ ├── MapboxMapFragment.kt
│ │ ├── MarkerFeature.kt
│ │ ├── StaticPolyLineFeature.kt
│ │ ├── StaticPolygonFeature.kt
│ │ └── TileHttpServer.java
│ └── res/
│ └── layout/
│ └── mapbox_fragment_layout.xml
├── maps/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── maps/
│ │ │ ├── AnalyticsEvents.kt
│ │ │ ├── MapConfigurator.kt
│ │ │ ├── MapConsts.kt
│ │ │ ├── MapFragment.kt
│ │ │ ├── MapFragmentFactory.kt
│ │ │ ├── MapPoint.kt
│ │ │ ├── MapViewModel.kt
│ │ │ ├── MapViewModelMapFragment.kt
│ │ │ ├── ZoomObserver.kt
│ │ │ ├── circles/
│ │ │ │ ├── CircleDescription.kt
│ │ │ │ └── CurrentLocationDelegate.kt
│ │ │ ├── layers/
│ │ │ │ ├── DirectoryReferenceLayerRepository.kt
│ │ │ │ ├── MapFragmentReferenceLayerUtils.kt
│ │ │ │ ├── MbtilesFile.java
│ │ │ │ ├── OfflineMapLayersImporterAdapter.kt
│ │ │ │ ├── OfflineMapLayersImporterDialogFragment.kt
│ │ │ │ ├── OfflineMapLayersPickerAdapter.kt
│ │ │ │ ├── OfflineMapLayersPickerBottomSheetDialogFragment.kt
│ │ │ │ ├── OfflineMapLayersViewModel.kt
│ │ │ │ ├── ReferenceLayerRepository.kt
│ │ │ │ └── TileSource.java
│ │ │ ├── markers/
│ │ │ │ ├── MarkerDescription.kt
│ │ │ │ ├── MarkerIconCreator.kt
│ │ │ │ └── MarkerIconDescription.kt
│ │ │ └── traces/
│ │ │ ├── LineDescription.kt
│ │ │ ├── PolygonDescription.kt
│ │ │ └── TraceDescription.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ └── ic_crosshairs.xml
│ │ └── layout/
│ │ ├── offline_map_layers_importer.xml
│ │ ├── offline_map_layers_importer_item.xml
│ │ ├── offline_map_layers_picker.xml
│ │ └── offline_map_layers_picker_item.xml
│ └── test/
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── maps/
│ │ ├── LineDescriptionTest.kt
│ │ ├── MarkerIconDescriptionTest.kt
│ │ ├── PolygonDescriptionTest.kt
│ │ ├── RobolectricApplication.kt
│ │ ├── TraceDescriptionTest.kt
│ │ └── layers/
│ │ ├── DirectoryReferenceLayerRepositoryTest.kt
│ │ ├── InMemReferenceLayerRepository.kt
│ │ ├── MapFragmentReferenceLayerUtilsTest.kt
│ │ ├── OfflineMapLayersImporterDialogFragmentTest.kt
│ │ └── OfflineMapLayersPickerBottomSheetDialogFragmentTest.kt
│ └── resources/
│ └── robolectric.properties
├── material/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── material/
│ │ │ ├── BottomSheetBehavior.kt
│ │ │ ├── ErrorsPill.kt
│ │ │ ├── MaterialFullScreenDialogFragment.kt
│ │ │ ├── MaterialPill.kt
│ │ │ ├── MaterialProgressDialogFragment.java
│ │ │ └── Pill.kt
│ │ └── res/
│ │ ├── layout/
│ │ │ ├── pill.xml
│ │ │ └── progress_dialog.xml
│ │ └── values/
│ │ ├── attrs.xml
│ │ ├── material_3_button_icon_end_style.xml
│ │ └── material_full_screen_dialog_theme.xml
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── material/
│ ├── ErrorsPillTest.kt
│ └── MaterialProgressDialogFragmentTest.java
├── metadata/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── metadata/
│ │ ├── InstallIDProvider.kt
│ │ └── PropertyManager.kt
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── metadata/
│ ├── PropertyManagerTest.kt
│ └── SettingsInstallIDProviderTest.kt
├── mobile-device-management/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── mobiledevicemanagement/
│ │ │ ├── MDMConfigHandler.kt
│ │ │ └── MDMConfigObserver.kt
│ │ └── res/
│ │ └── xml/
│ │ └── managed_configuration.xml
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── mobiledevicemanagement/
│ ├── MDMConfigHandlerTest.kt
│ └── MDMConfigObserverTest.kt
├── nbistubs/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ └── AndroidManifest.xml
├── open-rosa/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── openrosa/
│ │ ├── forms/
│ │ │ ├── DocumentFetchResult.java
│ │ │ ├── EntityIntegrity.kt
│ │ │ ├── OpenRosaClient.kt
│ │ │ └── OpenRosaXmlFetcher.java
│ │ ├── http/
│ │ │ ├── CaseInsensitiveEmptyHeaders.java
│ │ │ ├── CaseInsensitiveHeaders.java
│ │ │ ├── CollectThenSystemContentTypeMapper.java
│ │ │ ├── HttpCredentials.java
│ │ │ ├── HttpCredentialsInterface.java
│ │ │ ├── HttpGetResult.java
│ │ │ ├── HttpHeadResult.java
│ │ │ ├── HttpPostResult.java
│ │ │ ├── OpenRosaConstants.kt
│ │ │ ├── OpenRosaHttpInterface.java
│ │ │ └── okhttp/
│ │ │ ├── OkHttpCaseInsensitiveHeaders.java
│ │ │ ├── OkHttpConnection.java
│ │ │ ├── OkHttpOpenRosaServerClientProvider.java
│ │ │ ├── OpenRosaServerClient.java
│ │ │ └── OpenRosaServerClientProvider.java
│ │ └── parse/
│ │ ├── Kxml2OpenRosaResponseParser.kt
│ │ └── OpenRosaResponseParser.kt
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── openrosa/
│ ├── forms/
│ │ ├── OpenRosaClientTest.kt
│ │ └── OpenRosaXmlFetcherTest.java
│ ├── http/
│ │ ├── CaseInsensitiveEmptyHeadersTest.java
│ │ ├── CollectThenSystemContentTypeMapperTest.java
│ │ ├── OpenRosaGetRequestTest.java
│ │ ├── OpenRosaHeadRequestTest.java
│ │ ├── OpenRosaPostRequestTest.java
│ │ └── okhttp/
│ │ ├── OkHttpCaseInsensitiveHeadersTest.java
│ │ ├── OkHttpConnectionGetRequestTest.java
│ │ ├── OkHttpConnectionHeadRequestTest.java
│ │ ├── OkHttpConnectionPostRequestTest.java
│ │ ├── OkHttpOpenRosaServerClientProviderTest.java
│ │ └── OpenRosaServerClientProviderTest.java
│ ├── parse/
│ │ └── Kxml2OpenRosaResponseParserTest.kt
│ └── support/
│ ├── MockWebServerHelper.java
│ ├── MockWebServerRule.kt
│ └── StubWebCredentialsProvider.java
├── osmdroid/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── osmdroid/
│ │ ├── DaggerSetup.kt
│ │ ├── OsmDroidInitializer.kt
│ │ ├── OsmDroidMapConfigurator.java
│ │ ├── OsmDroidMapFragment.java
│ │ ├── OsmMBTileModuleProvider.java
│ │ ├── OsmMBTileProvider.java
│ │ ├── OsmMBTileSource.java
│ │ └── WebMapService.java
│ └── res/
│ └── layout/
│ └── osm_map_layout.xml
├── permissions/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── permissions/
│ │ │ ├── LocationAccessibilityChecker.kt
│ │ │ ├── PermissionListener.kt
│ │ │ ├── PermissionsChecker.kt
│ │ │ ├── PermissionsDialogCreator.kt
│ │ │ ├── PermissionsProvider.kt
│ │ │ └── RequestPermissionsAPI.kt
│ │ └── res/
│ │ └── drawable/
│ │ ├── ic_photo_camera.xml
│ │ ├── ic_room_24dp.xml
│ │ └── ic_storage.xml
│ └── test/
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── permissions/
│ │ ├── PermissionsDialogCreatorTest.kt
│ │ └── PermissionsProviderTest.kt
│ └── resources/
│ └── robolectric.properties
├── printer/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── printer/
│ └── HtmlPrinter.kt
├── projects/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── projects/
│ │ ├── DaggerSetup.kt
│ │ ├── InMemProjectsRepository.kt
│ │ ├── Project.kt
│ │ ├── ProjectConfigurationResult.kt
│ │ ├── ProjectCreator.kt
│ │ ├── ProjectDependencyFactory.kt
│ │ ├── ProjectsRepository.kt
│ │ ├── SettingsConnectionMatcher.kt
│ │ └── SharedPreferencesProjectsRepository.kt
│ └── test/
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── projects/
│ │ ├── InMemProjectsRepositoryTest.kt
│ │ ├── ProjectsRepositoryTest.kt
│ │ ├── SharedPreferencesProjectsRepositoryTest.kt
│ │ └── support/
│ │ └── RobolectricApplication.kt
│ └── resources/
│ └── robolectric.properties
├── qr-code/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── qrcode/
│ │ │ ├── BarcodeFilter.kt
│ │ │ ├── BarcodeScannerViewContainer.kt
│ │ │ ├── DetectedState.kt
│ │ │ ├── FlashlightToggle.kt
│ │ │ ├── ScannerControls.kt
│ │ │ ├── ScannerOverlay.kt
│ │ │ ├── mlkit/
│ │ │ │ ├── MlKitBarcodeScannerViewFactory.kt
│ │ │ │ └── PlayServicesFallbackBarcodeScannerViewFactory.kt
│ │ │ └── zxing/
│ │ │ ├── QRCodeCreator.kt
│ │ │ ├── QRCodeDecoder.kt
│ │ │ └── ZxingBarcodeScannerViewFactory.kt
│ │ └── res/
│ │ └── layout/
│ │ ├── mlkit_barcode_scanner_layout.xml
│ │ └── zxing_barcode_scanner_layout.xml
│ └── test/
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── qrcode/
│ │ ├── BarcodeFilterTest.kt
│ │ └── QRCodeEncodeDecodeTest.kt
│ └── resources/
│ └── robolectric.properties
├── secrets.gradle
├── selfie-camera/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── selfiecamera/
│ │ │ ├── Camera.kt
│ │ │ ├── CameraXCamera.kt
│ │ │ ├── CaptureSelfieActivity.kt
│ │ │ └── DaggerSetup.kt
│ │ └── res/
│ │ └── layout/
│ │ └── activity_capture_selfie.xml
│ └── test/
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── selfiecamera/
│ │ ├── CaptureSelfieActivityTest.kt
│ │ └── support/
│ │ └── RobolectricApplication.kt
│ └── resources/
│ └── robolectric.properties
├── service-test/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── servicetest/
│ ├── NotificationDetails.kt
│ └── ServiceScenario.kt
├── settings/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── settings/
│ │ │ ├── InMemSettingsProvider.kt
│ │ │ ├── ODKAppSettingsImporter.kt
│ │ │ ├── ODKAppSettingsMigrator.java
│ │ │ ├── SettingsProvider.kt
│ │ │ ├── enums/
│ │ │ │ ├── AutoSend.kt
│ │ │ │ ├── FormUpdateMode.java
│ │ │ │ ├── GuidanceHintMode.kt
│ │ │ │ ├── StringIdEnum.kt
│ │ │ │ └── StringIdEnumUtils.kt
│ │ │ ├── importing/
│ │ │ │ ├── ProjectDetailsCreatorImpl.kt
│ │ │ │ └── SettingsImporter.kt
│ │ │ ├── keys/
│ │ │ │ ├── AppConfigurationKeys.kt
│ │ │ │ ├── MetaKeys.kt
│ │ │ │ ├── ProjectKeys.kt
│ │ │ │ └── ProtectedProjectKeys.kt
│ │ │ ├── migration/
│ │ │ │ ├── KeyCombiner.java
│ │ │ │ ├── KeyExtractor.java
│ │ │ │ ├── KeyMover.java
│ │ │ │ ├── KeyRenamer.java
│ │ │ │ ├── KeyTranslator.java
│ │ │ │ ├── KeyUpdater.java
│ │ │ │ ├── KeyValuePair.java
│ │ │ │ ├── Migration.java
│ │ │ │ ├── MigrationUtils.java
│ │ │ │ └── ValueTranslator.java
│ │ │ └── validation/
│ │ │ └── JsonSchemaSettingsValidator.kt
│ │ ├── res/
│ │ │ └── values/
│ │ │ └── strings.xml
│ │ └── resources/
│ │ └── client-settings.schema.json
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── settings/
│ ├── ODKAppSettingsImporterTest.kt
│ ├── ODKAppSettingsMigratorTest.java
│ ├── importing/
│ │ ├── ProjectDetailsCreatorImplTest.kt
│ │ └── SettingsImporterTest.kt
│ ├── migration/
│ │ ├── KeyCombinerTest.java
│ │ ├── KeyExtractorTest.java
│ │ ├── KeyMoverTest.java
│ │ ├── KeyRemoverTest.java
│ │ ├── KeyRenamerTest.java
│ │ ├── KeyTranslatorTest.java
│ │ └── ValueTranslatorTest.java
│ ├── support/
│ │ └── SettingsUtils.kt
│ └── validation/
│ ├── JsonSchemaSettingsValidatorTest.kt
│ └── OriginalJsonSchemaSettingsValidatorTest.kt
├── settings.gradle
├── shadows/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── shadows/
│ │ └── ShadowAndroidXAlertDialog.kt
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── shadows/
│ └── ShadowAndroidXAlertDialogTest.kt
├── shared/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── shared/
│ │ ├── DebugLogger.kt
│ │ ├── PathUtils.kt
│ │ ├── Query.kt
│ │ ├── TempFiles.kt
│ │ ├── TimeInMs.kt
│ │ ├── collections/
│ │ │ └── CollectionExtensions.kt
│ │ ├── files/
│ │ │ └── FileExt.kt
│ │ ├── geometry/
│ │ │ └── Geometry.kt
│ │ ├── injection/
│ │ │ └── ObjectProvider.kt
│ │ ├── locks/
│ │ │ ├── BooleanChangeLock.kt
│ │ │ ├── ChangeLock.kt
│ │ │ └── ThreadSafeBooleanChangeLock.kt
│ │ ├── result/
│ │ │ └── Result.kt
│ │ ├── settings/
│ │ │ ├── InMemSettings.kt
│ │ │ └── Settings.kt
│ │ └── strings/
│ │ ├── Md5.kt
│ │ ├── RandomString.java
│ │ ├── StringUtils.kt
│ │ └── UUIDGenerator.kt
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── shared/
│ ├── Md5Test.kt
│ ├── PathUtilsTest.kt
│ ├── QuickCheck.kt
│ ├── collections/
│ │ └── CollectionExtensionsTest.kt
│ ├── files/
│ │ └── FileExtTest.kt
│ ├── geometry/
│ │ ├── GeometryTest.kt
│ │ └── support/
│ │ ├── GeometryTestUtils.kt
│ │ └── GeometryTestUtilsTest.kt
│ ├── locks/
│ │ ├── BooleanChangeLockTest.kt
│ │ ├── ChangeLockTest.kt
│ │ └── ThreadSafeBooleanChangeLockTest.kt
│ └── strings/
│ └── StringUtilsTest.kt
├── strings/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── odk/
│ │ │ └── collect/
│ │ │ └── strings/
│ │ │ ├── format/
│ │ │ │ └── LengthFormatter.kt
│ │ │ └── localization/
│ │ │ ├── LocalizedActivity.kt
│ │ │ └── LocalizedApplication.kt
│ │ └── res/
│ │ ├── values/
│ │ │ ├── strings.xml
│ │ │ └── untranslated.xml
│ │ ├── values-af/
│ │ │ └── strings.xml
│ │ ├── values-am/
│ │ │ └── strings.xml
│ │ ├── values-ar/
│ │ │ └── strings.xml
│ │ ├── values-bg/
│ │ │ └── strings.xml
│ │ ├── values-bn/
│ │ │ └── strings.xml
│ │ ├── values-ca/
│ │ │ └── strings.xml
│ │ ├── values-cs/
│ │ │ └── strings.xml
│ │ ├── values-da/
│ │ │ └── strings.xml
│ │ ├── values-de/
│ │ │ └── strings.xml
│ │ ├── values-es/
│ │ │ └── strings.xml
│ │ ├── values-es-rSV/
│ │ │ └── strings.xml
│ │ ├── values-et/
│ │ │ └── strings.xml
│ │ ├── values-fa/
│ │ │ └── strings.xml
│ │ ├── values-fa-rAF/
│ │ │ └── strings.xml
│ │ ├── values-fi/
│ │ │ └── strings.xml
│ │ ├── values-fr/
│ │ │ └── strings.xml
│ │ ├── values-hi/
│ │ │ └── strings.xml
│ │ ├── values-ht/
│ │ │ └── strings.xml
│ │ ├── values-in/
│ │ │ └── strings.xml
│ │ ├── values-it/
│ │ │ └── strings.xml
│ │ ├── values-ja/
│ │ │ └── strings.xml
│ │ ├── values-ka/
│ │ │ └── strings.xml
│ │ ├── values-km/
│ │ │ └── strings.xml
│ │ ├── values-ln/
│ │ │ └── strings.xml
│ │ ├── values-lo-rLA/
│ │ │ └── strings.xml
│ │ ├── values-lt/
│ │ │ └── strings.xml
│ │ ├── values-mg/
│ │ │ └── strings.xml
│ │ ├── values-ml/
│ │ │ └── strings.xml
│ │ ├── values-mr/
│ │ │ └── strings.xml
│ │ ├── values-ms/
│ │ │ └── strings.xml
│ │ ├── values-my/
│ │ │ └── strings.xml
│ │ ├── values-ne-rNP/
│ │ │ └── strings.xml
│ │ ├── values-nl/
│ │ │ └── strings.xml
│ │ ├── values-no/
│ │ │ └── strings.xml
│ │ ├── values-pl/
│ │ │ └── strings.xml
│ │ ├── values-ps/
│ │ │ └── strings.xml
│ │ ├── values-pt/
│ │ │ └── strings.xml
│ │ ├── values-ro/
│ │ │ └── strings.xml
│ │ ├── values-ru/
│ │ │ └── strings.xml
│ │ ├── values-rw/
│ │ │ └── strings.xml
│ │ ├── values-si/
│ │ │ └── strings.xml
│ │ ├── values-sl/
│ │ │ └── strings.xml
│ │ ├── values-so/
│ │ │ └── strings.xml
│ │ ├── values-sq/
│ │ │ └── strings.xml
│ │ ├── values-sr/
│ │ │ └── strings.xml
│ │ ├── values-sv-rSE/
│ │ │ └── strings.xml
│ │ ├── values-sw/
│ │ │ └── strings.xml
│ │ ├── values-sw-rKE/
│ │ │ └── strings.xml
│ │ ├── values-te/
│ │ │ └── strings.xml
│ │ ├── values-th-rTH/
│ │ │ └── strings.xml
│ │ ├── values-ti/
│ │ │ └── strings.xml
│ │ ├── values-tl/
│ │ │ └── strings.xml
│ │ ├── values-tl-rPH/
│ │ │ └── strings.xml
│ │ ├── values-tr/
│ │ │ └── strings.xml
│ │ ├── values-uk/
│ │ │ └── strings.xml
│ │ ├── values-ur/
│ │ │ └── strings.xml
│ │ ├── values-ur-rPK/
│ │ │ └── strings.xml
│ │ ├── values-vi/
│ │ │ └── strings.xml
│ │ ├── values-zh/
│ │ │ └── strings.xml
│ │ ├── values-zh-rTW/
│ │ │ └── strings.xml
│ │ └── values-zu/
│ │ └── strings.xml
│ └── test/
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── strings/
│ │ └── format/
│ │ ├── DateFormatsTest.kt
│ │ └── LengthFormatterTest.kt
│ └── resources/
│ └── robolectric.properties
├── test-forms/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ └── resources/
│ ├── forms/
│ │ ├── Empty First Repeat.xml
│ │ ├── RepeatGroupAndGroup.xml
│ │ ├── RepeatTitles_1648.xml
│ │ ├── TestRepeat.xml
│ │ ├── all-widgets.xml
│ │ ├── audio-question.xml
│ │ ├── basic.xml
│ │ ├── different-search-appearances.xml
│ │ ├── dynamic_and_static_choices.xml
│ │ ├── dynamic_required_question.xml
│ │ ├── emptyGroupFieldList.xml
│ │ ├── encrypted-no-instanceID.xml
│ │ ├── encrypted.xml
│ │ ├── entity-update-pulldata.xml
│ │ ├── external-audio-question.xml
│ │ ├── external-csv-search-broken.xml
│ │ ├── external-csv-search.xml
│ │ ├── external_csv_form.xml
│ │ ├── external_data_questions.xml
│ │ ├── external_select.xml
│ │ ├── external_select_10.xml
│ │ ├── external_select_csv.xml
│ │ ├── field-list-repeat.xml
│ │ ├── fieldlist-updates.xml
│ │ ├── fixed-count-repeat.xml
│ │ ├── form1.xml
│ │ ├── form2.xml
│ │ ├── form3.xml
│ │ ├── form4.xml
│ │ ├── form5.xml
│ │ ├── form6.xml
│ │ ├── form7.xml
│ │ ├── form8.xml
│ │ ├── form9.xml
│ │ ├── formHierarchy1.xml
│ │ ├── formHierarchy2.xml
│ │ ├── formHierarchy3.xml
│ │ ├── form_design_error.xml
│ │ ├── form_styling.xml
│ │ ├── form_with_images.xml
│ │ ├── g6Error.xml
│ │ ├── hints_textq.xml
│ │ ├── identify-user-audit-false.xml
│ │ ├── identify-user-audit.xml
│ │ ├── intent-group.xml
│ │ ├── internal-audio-question.xml
│ │ ├── invalid-form.xml
│ │ ├── likert_test.xml
│ │ ├── location-audit.xml
│ │ ├── manyQ.xml
│ │ ├── metadata.xml
│ │ ├── nested-intent-group.xml
│ │ ├── numberInCSV.xml
│ │ ├── one-question-audit.xml
│ │ ├── one-question-autoplay.xml
│ │ ├── one-question-autosend-disabled.xml
│ │ ├── one-question-autosend.xml
│ │ ├── one-question-background-audio-audit.xml
│ │ ├── one-question-background-audio-multiple.xml
│ │ ├── one-question-background-audio.xml
│ │ ├── one-question-editable.xml
│ │ ├── one-question-encrypted-unicode.xml
│ │ ├── one-question-entity-create-and-update.xml
│ │ ├── one-question-entity-follow-up.xml
│ │ ├── one-question-entity-registration-broken.xml
│ │ ├── one-question-entity-registration-editable.xml
│ │ ├── one-question-entity-registration-id.xml
│ │ ├── one-question-entity-registration-v2020.1.xml
│ │ ├── one-question-entity-registration-v2023.1.xml
│ │ ├── one-question-entity-registration.xml
│ │ ├── one-question-entity-update-and-create.xml
│ │ ├── one-question-entity-update-editable.xml
│ │ ├── one-question-entity-update.xml
│ │ ├── one-question-last-saved-updated.xml
│ │ ├── one-question-last-saved.xml
│ │ ├── one-question-partial.xml
│ │ ├── one-question-repeat.xml
│ │ ├── one-question-translation.xml
│ │ ├── one-question-updated.xml
│ │ ├── one-question-uuid-instance-name.xml
│ │ ├── one-question-with-constraint.xml
│ │ ├── one-question.xml
│ │ ├── pull_data.xml
│ │ ├── random.xml
│ │ ├── randomTest_broken.xml
│ │ ├── ranking_widget.xml
│ │ ├── repeat_group_form.xml
│ │ ├── repeat_group_new.xml
│ │ ├── repeat_group_wrapped_with_a_regular_group.xml
│ │ ├── repeat_groups.xml
│ │ ├── repeat_in_field_list.xml
│ │ ├── repeat_without_label.xml
│ │ ├── requiredQuestionInFieldList.xml
│ │ ├── required_question_with_audio.xml
│ │ ├── required_question_with_custom_error_message.xml
│ │ ├── search_and_select.xml
│ │ ├── selectOneExternal.xml
│ │ ├── select_one_external.xml
│ │ ├── setgeopoint-action.xml
│ │ ├── simple-search-external-csv.xml
│ │ ├── single-geopoint.xml
│ │ ├── start-geopoint.xml
│ │ ├── string_widgets_in_field_list.xml
│ │ ├── track-changes-reason-on-edit.xml
│ │ ├── two-question-audit-track-changes.xml
│ │ ├── two-question-audit.xml
│ │ ├── two-question-external.xml
│ │ ├── two-question-required.xml
│ │ ├── two-question-save-incomplete-required.xml
│ │ ├── two-question-save-incomplete.xml
│ │ ├── two-question-updated.xml
│ │ ├── two-question.xml
│ │ ├── two-questions-in-group.xml
│ │ └── validate.xml
│ └── media/
│ ├── external-csv-search-produce.csv
│ ├── external_csv_cities.csv
│ ├── external_csv_countries.csv
│ ├── external_csv_neighbourhoods.csv
│ ├── external_data.csv
│ ├── external_data.xml
│ ├── external_data_10.xml
│ ├── external_data_broken.csv
│ ├── fruits.csv
│ ├── itemSets.csv
│ ├── people.csv
│ ├── selectOneExternal-media/
│ │ └── itemsets.csv
│ ├── simple-search-external-csv-fruits.csv
│ ├── test.m4a
│ └── updated-people.csv
├── test-shared/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── testshared/
│ ├── ActivityControllerRule.kt
│ ├── ActivityExt.kt
│ ├── AssertIntentsHelper.kt
│ ├── AssertionFramework.kt
│ ├── ComposeAssertions.kt
│ ├── ComposeInteractions.kt
│ ├── DummyActivity.kt
│ ├── ErrorIntentLauncher.kt
│ ├── EspressoAssertions.kt
│ ├── EspressoInteractions.kt
│ ├── FakeAudioPlayer.kt
│ ├── FakeBarcodeScannerView.kt
│ ├── FakeBroadcastReceiverRegister.kt
│ ├── FakeScheduler.kt
│ ├── FragmentResultRecorder.kt
│ ├── LocationTestUtils.kt
│ ├── MockFragmentFactory.kt
│ ├── MockWebPageService.kt
│ ├── RecyclerViewMatcher.kt
│ ├── RobolectricHelpers.kt
│ ├── SliderExt.kt
│ ├── TimeZoneSetter.kt
│ ├── ViewActions.kt
│ ├── ViewMatchers.kt
│ └── WaitFor.kt
├── timedgrid/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── timedgrid/
│ │ ├── AssessmentType.kt
│ │ ├── CommonTimedGridRenderer.kt
│ │ ├── FinishType.kt
│ │ ├── NavigationAwareWidget.kt
│ │ ├── PausableCountDownTimer.kt
│ │ ├── TimedGridRenderer.kt
│ │ ├── TimedGridState.kt
│ │ ├── TimedGridSummary.kt
│ │ ├── TimedGridSummaryAnswerCreator.kt
│ │ ├── TimedGridViewModel.kt
│ │ ├── TimedGridWidgetConfiguration.kt
│ │ ├── TimedGridWidgetDelegate.kt
│ │ └── TimedGridWidgetLayout.kt
│ └── res/
│ ├── color/
│ │ └── timed_grid_button_tint_selector.xml
│ ├── drawable/
│ │ └── row_number_background.xml
│ ├── layout/
│ │ ├── timed_grid.xml
│ │ ├── timed_grid_item_button.xml
│ │ └── timed_grid_item_row.xml
│ └── values/
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
├── upgrade/
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── org/
│ │ └── odk/
│ │ └── collect/
│ │ └── upgrade/
│ │ ├── AppUpgrader.kt
│ │ ├── LaunchState.kt
│ │ └── Upgrade.kt
│ └── test/
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── upgrade/
│ ├── AppUpgraderTest.kt
│ └── VersionCodeLaunchStateTest.kt
└── web-page/
├── .gitignore
├── build.gradle.kts
└── src/
├── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── org/
│ └── odk/
│ └── collect/
│ └── webpage/
│ ├── CustomTabsWebPageService.kt
│ └── WebPageService.kt
└── test/
└── java/
└── org/
└── odk/
└── collect/
└── webpage/
└── CustomTabsWebPageServiceTest.java
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
# This config and the the Gradle flags/opts are based on: https://circleci.com/docs/2.0/language-android/
# and https://support.circleci.com/hc/en-us/articles/360021812453
version: 2
references:
android_config_small: &android_config_small
working_directory: ~/work
docker:
- image: cimg/android:2026.02
resource_class: small
android_config: &android_config
working_directory: ~/work
docker:
- image: cimg/android:2026.02
resource_class: large
android_config_large: &android_config_large
working_directory: ~/work
docker:
- image: cimg/android:2026.02
resource_class: xlarge
gcloud_config: &gcloud_config
working_directory: ~/work
docker:
- image: cimg/gcp:2025.01
resource_class: small
jobs:
compile:
<<: *android_config
steps:
- checkout
- restore_cache:
keys:
- compile-{{ checksum "gradle/libs.versions.toml" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
- compile-
- run:
name: Copy gradle config
command: mkdir -p ~/.gradle && cp .circleci/gradle.properties ~/.gradle/gradle.properties
- run:
name: Download Robolectric deps
command: ./download-robolectric-deps.sh
- run:
name: Compile code
command: ./gradlew assembleDebug
- save_cache:
paths:
- ~/.gradle/caches
- ~/.gradle/wrapper
key: compile-{{ checksum "gradle/libs.versions.toml" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
- save_cache:
paths:
- ~/work
key: workflow-{{ .Environment.CIRCLE_WORKFLOW_ID }}
- persist_to_workspace:
root: ~/work
paths:
- collect_app/build/outputs/apk
create_dependency_backup:
<<: *android_config_small
steps:
- checkout
- restore_cache:
keys:
- workflow-{{ .Environment.CIRCLE_WORKFLOW_ID }}
- restore_cache:
keys:
- compile-{{ checksum "gradle/libs.versions.toml" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
- compile-
- run:
name: Create Maven repo from dependencies
command: ./gradlew cacheToMavenLocal
- run:
name: Compress Maven repo
command: tar -cvzf maven.tar .local-m2
- store_artifacts:
path: maven.tar
check_quality:
<<: *android_config
steps:
- checkout
- restore_cache:
keys:
- workflow-{{ .Environment.CIRCLE_WORKFLOW_ID }}
- restore_cache:
keys:
- compile-{{ checksum "gradle/libs.versions.toml" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
- compile-
- run:
name: Copy gradle config
command: mkdir -p ~/.gradle && cp .circleci/gradle.properties ~/.gradle/gradle.properties
- run:
name: Run code quality checks
command: ./gradlew pmd ktlintCheck checkstyle
- run:
name: Run Android lint
command: ./gradlew lintDebug
test_modules:
<<: *android_config
parallelism: 4
steps:
- checkout
- restore_cache:
keys:
- workflow-{{ .Environment.CIRCLE_WORKFLOW_ID }}
- restore_cache:
keys:
- test_modules-{{ checksum "gradle/libs.versions.toml" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
- test_modules
- compile-{{ checksum "gradle/libs.versions.toml" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
- compile-
- run:
name: Copy gradle config
command: mkdir -p ~/.gradle && cp .circleci/gradle.properties ~/.gradle/gradle.properties
- run:
name: Generate list of modules for this fork
command: |
cat .circleci/test_modules.txt | circleci tests split > .circleci/fork_test_modules.txt && \
echo "Modules for this fork:" && \
cat .circleci/fork_test_modules.txt
- run:
name: Run module unit tests
command: |
./gradlew $(cat .circleci/fork_test_modules.txt | awk '{for (i=1; i<=NF; i++) printf "%s:testDebug ",$i}')
- store_test_results:
path: collect_app/build/test-results
- save_cache:
paths:
- ~/.gradle/caches
- ~/.gradle/wrapper
key: test_modules-{{ checksum "gradle/libs.versions.toml" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
test_app:
<<: *android_config
parallelism: 4
steps:
- checkout
- restore_cache:
keys:
- workflow-{{ .Environment.CIRCLE_WORKFLOW_ID }}
- restore_cache:
keys:
- test_app-{{ checksum "gradle/libs.versions.toml" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
- test_app-
- compile-{{ checksum "gradle/libs.versions.toml" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
- compile-
- run:
name: Copy gradle config
command: mkdir -p ~/.gradle && cp .circleci/gradle.properties ~/.gradle/gradle.properties
- run:
name: Generate list of test classes
command: .circleci/generate-app-test-list.sh
- run:
name: Generate list of tests for this fork
command: |
cat .circleci/collect_app_test_classes.txt | circleci tests split > .circleci/fork_test_classes.txt && \
echo "Tests for this fork:" && \
cat .circleci/fork_test_classes.txt && \
echo "" && \
echo "Will run command:" && \
echo "./gradlew collect_app:testDebug $(cat .circleci/fork_test_classes.txt | awk '{for (i=1; i<=NF; i++) printf "--tests %s ",$i}')"
- run:
name: Run app unit tests
command: |
./gradlew collect_app:testDebug $(cat .circleci/fork_test_classes.txt | awk '{for (i=1; i<=NF; i++) printf "--tests %s ",$i}')
- store_artifacts:
path: collect_app/build/reports
destination: reports
- store_test_results:
path: collect_app/build/test-results
- save_cache:
paths:
- ~/.gradle/caches
- ~/.gradle/wrapper
key: test_app-{{ checksum "gradle/libs.versions.toml" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
build_instrumented:
<<: *android_config_large
steps:
- checkout
- restore_cache:
keys:
- workflow-{{ .Environment.CIRCLE_WORKFLOW_ID }}
- restore_cache:
keys:
- build_instrumented-{{ checksum "gradle/libs.versions.toml" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
- build_instrumented-
- compile-{{ checksum "gradle/libs.versions.toml" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
- compile-
- run:
name: Copy gradle config
command: mkdir -p ~/.gradle && cp .circleci/gradle-large.properties ~/.gradle/gradle.properties
- run:
name: Assemble connected test build
command: ./gradlew assembleDebugAndroidTest
- save_cache:
paths:
- ~/.gradle/caches
- ~/.gradle/wrapper
key: build_instrumented-{{ checksum "gradle/libs.versions.toml" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
- persist_to_workspace:
root: ~/work
paths:
- collect_app/build/outputs/apk
build_release:
<<: *android_config
steps:
- checkout
- restore_cache:
keys:
- workflow-{{ .Environment.CIRCLE_WORKFLOW_ID }}
- restore_cache:
keys:
- compile-{{ checksum "gradle/libs.versions.toml" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
- compile-
- run:
name: Copy gradle config
command: mkdir -p ~/.gradle && cp .circleci/gradle.properties ~/.gradle/gradle.properties
- run:
name: Set up secrets.properties for Maps frameworks
command: |
if [[ -n "$GOOGLE_MAPS_API_KEY" ]]; then \
echo "GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY" >> secrets.properties
echo "MAPBOX_ACCESS_TOKEN=$MAPBOX_ACCESS_TOKEN" >> secrets.properties
echo "MAPBOX_DOWNLOADS_TOKEN=$MAPBOX_DOWNLOADS_TOKEN" >> secrets.properties
fi
- run:
name: Assemble self signed release build
command: ./gradlew assembleSelfSignedRelease
- run:
name: Check APK size hasn't increased
command: |
if [[ -n "$GOOGLE_MAPS_API_KEY" ]]; then \
./check-size.sh 20447232
else
./check-size.sh 11010048
fi
- run:
name: Copy APK to predictable path for artifact storage
command: cp collect_app/build/outputs/apk/selfSignedRelease/*.apk selfSignedRelease.apk
- store_artifacts:
path: selfSignedRelease.apk
test_smoke_instrumented:
<<: *gcloud_config
steps:
- attach_workspace:
at: /tmp/workspace
- run:
name: Authorize gcloud
command: |
if [[ "$CIRCLE_PROJECT_USERNAME" == "getodk" ]]; then \
gcloud config set project api-project-322300403941
echo $GCLOUD_SERVICE_KEY | base64 --decode > client-secret.json
gcloud auth activate-service-account --key-file client-secret.json
fi
- run:
name: Run integration tests
command: |
if [[ "$CIRCLE_PROJECT_USERNAME" == "getodk" ]]; then \
echo "y" | gcloud beta firebase test android run \
--type instrumentation \
--app /tmp/workspace/collect_app/build/outputs/apk/debug/ODK-Collect-debug.apk \
--test /tmp/workspace/collect_app/build/outputs/apk/androidTest/debug/ODK-Collect-debug-androidTest.apk \
--device model=MediumPhone.arm,version=34,locale=en,orientation=portrait \
--results-bucket opendatakit-collect-test-results \
--directories-to-pull /sdcard --timeout 20m \
--test-targets "package org.odk.collect.android.feature.smoke"
fi
no_output_timeout: 25m
test_instrumented:
<<: *gcloud_config
steps:
- attach_workspace:
at: /tmp/workspace
- run:
name: Authorize gcloud
command: |
if [[ -n "$GCLOUD_SERVICE_KEY" ]]; then \
gcloud config set project api-project-322300403941
echo $GCLOUD_SERVICE_KEY | base64 --decode > client-secret.json
gcloud auth activate-service-account --key-file client-secret.json
fi
- run:
name: Run integration tests
command: |
echo "y" | gcloud beta firebase test android run \
--type instrumentation \
--num-uniform-shards=25 \
--app /tmp/workspace/collect_app/build/outputs/apk/debug/ODK-Collect-debug.apk \
--test /tmp/workspace/collect_app/build/outputs/apk/androidTest/debug/ODK-Collect-debug-androidTest.apk \
--device model=MediumPhone.arm,version=34,locale=en,orientation=portrait \
--results-bucket opendatakit-collect-test-results \
--directories-to-pull /sdcard --timeout 20m \
--test-targets "notPackage org.odk.collect.android.regression" \
--test-targets "notPackage org.odk.collect.android.benchmark"
no_output_timeout: 25m
workflows:
version: 2
pr:
jobs:
- hold_pr:
type: approval
filters:
branches:
ignore:
- master
- ^v((20)[0-9]{2})\.\d+\.x$
- compile:
requires:
- hold_pr
- check_quality:
requires:
- compile
- test_modules:
requires:
- compile
- test_app:
requires:
- compile
- build_release:
requires:
- compile
- build_instrumented:
requires:
- compile
master:
jobs:
- compile:
filters:
branches:
only:
- master
- ^v((20)[0-9]{2})\.\d+\.x$
- check_quality:
requires:
- compile
- test_modules:
requires:
- compile
- test_app:
requires:
- compile
- build_release:
requires:
- compile
- build_instrumented:
requires:
- compile
- test_smoke_instrumented:
requires:
- build_instrumented
nightly:
triggers:
- schedule:
cron: "0 0 * * 1-5"
filters:
branches:
only:
- master
jobs:
- compile
- build_instrumented:
requires:
- compile
- test_instrumented:
requires:
- build_instrumented
release:
jobs:
- compile:
filters:
tags:
only: /^v((20)[0-9]{2})\.\d+\.\d+$/ # matches semvers like v1.2.3
branches:
ignore: /.*/
- create_dependency_backup:
requires:
- compile
filters:
tags:
only: /^v((20)[0-9]{2})\.\d+\.\d+$/ # matches semvers like v1.2.3
branches:
ignore: /.*/
================================================
FILE: .circleci/generate-app-test-list.sh
================================================
ls -R collect_app/src/test/java/ | grep Test.java > .circleci/collect_app_test_files.txt
ls -R collect_app/src/test/java/ | grep Test.kt >> .circleci/collect_app_test_files.txt
cat .circleci/collect_app_test_files.txt | sed "s/\.java//" | sed "s/\.kt//" > .circleci/collect_app_test_classes.txt
================================================
FILE: .circleci/gradle-large.properties
================================================
# Gradle config for "X-Large" Circle CI resource (https://circleci.com/pricing/price-list/)
org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options=-Xmx2g
org.gradle.daemon=false
org.gradle.parallel=true
org.gradle.workers.max=8
test.heap.max=1g
================================================
FILE: .circleci/gradle.properties
================================================
# Gradle config for "Large" Circle CI resource (https://circleci.com/pricing/price-list/)
org.gradle.jvmargs=-Xmx2560m -XX:MaxMetaspaceSize=1g
org.gradle.daemon=false
org.gradle.parallel=true
org.gradle.workers.max=4
test.heap.max=1g
================================================
FILE: .circleci/test_modules.txt
================================================
shared
forms-test
androidshared
async
strings
audio-clips
audio-recorder
projects
location
geo
upgrade
permissions
settings
maps
errors
crash-handler
entities
qr-code
shadows
metadata
selfie-camera
draw
printer
lists
web-page
open-rosa
mobile-device-management
material
================================================
FILE: .editorconfig
================================================
root = true
[*.{kt,kts}]
ktlint_standard_no-blank-lines-in-chained-method-calls = disabled
ktlint_standard_trailing-comma-on-call-site = disabled
ktlint_standard_trailing-comma-on-declaration-site = disabled
ktlint_standard_function-signature = disabled
ktlint_standard_no-empty-first-line-in-class-body = disabled
ktlint_standard_argument-list-wrapping = disabled
ktlint_standard_parameter-list-wrapping = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_max-line-length = disabled
ktlint_standard_string-template-indent = disabled
ktlint_standard_annotation = disabled
ktlint_standard_value-parameter-comment = disabled
ktlint_standard_property-naming = disabled
ktlint_standard_value-argument-comment = disabled
ktlint_standard_blank-line-before-declaration = disabled
ktlint_standard_no-consecutive-comments = disabled
ktlint_standard_enum-wrapping = disabled
ktlint_standard_statement-wrapping = disabled
ktlint_standard_try-catch-finally-spacing = disabled
ktlint_standard_wrapping = disabled
ktlint_standard_chain-method-continuation = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_class-signature = disabled
ktlint_standard_binary-expression-wrapping = disabled
ktlint_standard_condition-wrapping = disabled
ktlint_standard_function-literal = disabled
ktlint_standard_backing-property-naming = disabled
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_no-unused-imports=enabled
================================================
FILE: .gitattributes
================================================
* text=lf
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
Please refer to the project-wide [ODK Code of Conduct](https://github.com/getodk/governance/blob/master/CODE-OF-CONDUCT.md).
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true
contact_links:
- name: "Report an issue"
about: "For when Collect is behaving in an unexpected way"
url: "https://forum.getodk.org/c/support/6"
- name: "Request a feature"
about: "For when Collect is missing functionality"
url: "https://forum.getodk.org/c/features/9"
- name: "Everything else"
about: "For everything else"
url: "https://forum.getodk.org/c/support/6"
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
#### ODK Collect version
#### Android version
#### Device used
#### Problem description
#### Steps to reproduce the problem
#### Expected behavior
#### Other information
Things you tried, stack traces, related issues, suggestions on how to fix it...
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
Closes #
#### Why is this the best possible solution? Were any other approaches considered?
#### How does this change affect users? Describe intentional changes to behavior and behavior that could have accidentally been affected by code changes. In other words, what are the regression risks?
#### Do we need any specific form for testing your changes? If so, please attach one.
#### Does this change require updates to documentation? If so, please file an issue [here]( https://github.com/getodk/docs/issues/new) and include the link below.
#### Before submitting this PR, please make sure you have:
- [ ] added or modified tests for any new or changed behavior
- [ ] run `./gradlew connectedAndroidTest` (or `./gradlew testLab`) and confirmed all checks still pass
- [ ] added a comment above any new strings describing it for translators
- [ ] added any new strings with date formatting to `DateFormatsTest`
- [ ] verified that any code or assets from external sources are properly credited in comments and/or in the [about file](https://github.com/getodk/collect/blob/master/collect_app/src/main/assets/open_source_licenses.html).
- [ ] verified that any new UI elements use theme colors. [UI Components Style guidelines](https://github.com/getodk/collect/blob/master/docs/CODE-GUIDELINES.md#ui-components-style-guidelines)
================================================
FILE: .github/TESTING_RESULT_TEMPLATES.md
================================================
# Testing result templates
## Tested with success!
#### Verified on: [List of devices/os versions]
#### Verified cases: [optional]
***
## Bug[s] has[have] been found! / Regression[s] has[have] been found!
#### Verified on: [List of devices/os versions]
#### Problem visible on: [only if it's related to some specific devices otherwise it’s the same as above]
#### Steps to reproduce:
#### Current behavior:
#### Expected behavior:
#### Other information: [optional]
================================================
FILE: .gitignore
================================================
build
.gradle
.idea
.kotlin
local.properties
*.iml
.DS_Store
*.sublime-project
# emacs backup files
*.*~
# gradle env props
secrets.properties
# built application files
*.apk
*.ap_
# files for the Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Android Studio files
.externalNativeBuild/
.navigation/
captures/
# Visual Studio Code files
.project
.settings/
collect_app/.classpath
collect_app/.project
collect_app/.settings/
# Config for the official ODK Collect build
collect_app/src/odkCollectRelease/
# Files generated during CI runs
.circleci/collect_app_test_files.txt
.circleci/collect_app_test_classes.txt
.circleci/fork_test_classes.txt
# Robolectric dependencies
robolectric-deps/
**/robolectric-deps.properties
.local-m2
apks
================================================
FILE: .hgtags
================================================
5836a17a0e6f28b4e5b7a7abc9152446f4806ee2 v1.0.0
30d9314729db181177bb7209d1c16cd65a5095eb v1.1.0
827cf67bd902b80c37dce8d4ee8d2d29437408a5 v1.1.1
82798e410f5ab043721b3998e23861fd365a9801 v1.1.2
78fc6b2df0575ec79b94c0196babacbafcb7f958 v1.1.3
c6308d8be8cd99d060a06f3b10c87f5722776098 v1.1.4
f85cab690aa86dc86c2d53f12def22f16d9c0c5b v1.1.7-beta1
165ee1feda811cccefda77ae83896b7cb3209b6c v1.1.7-rc1
0c06023f4bae172a79a5aad27ca8d66798f2109e v1.1.7-rc2
caeb88584771e1e569c87eed7cca90738c867189 v1.1.7
96568099c2ec1408508178568fc5ca2503ec670b v1.2 RC1
96568099c2ec1408508178568fc5ca2503ec670b v1.2 RC1
15185704d55c220b02abcf5de6170a106f387f5b v1.2 RC1
88476e4dc9845a0a276bb57f93911d8aacc68912 v1.2.0
88476e4dc9845a0a276bb57f93911d8aacc68912 v1.2.0
ccb4a2556950af32ac153c26ca1f71faebbf8a9b v1.2.0
ccb4a2556950af32ac153c26ca1f71faebbf8a9b v1.2.0
2a9b44755f0a5af2558b7e22ac9948377b6e9597 v1.2.0
2a9b44755f0a5af2558b7e22ac9948377b6e9597 v1.2.0
a5faa6055d4bdbc5db786673c28d9872142120c7 v1.2.0
a5faa6055d4bdbc5db786673c28d9872142120c7 v1.2.0
0000000000000000000000000000000000000000 v1.2.0
b0ac9ab2b5bccea1b160e774b28a1f0c8af9a9e8 v1.2.0 rev 1012
3e7860308f3d7520ff91b8fb27bc5123450cfaa4 v1.2.0 rev 1013
3e7860308f3d7520ff91b8fb27bc5123450cfaa4 v1.2.0 rev 1013
b7d1fccb1a29ae62185bef1a123e80dbcdbc171c v1.2.0 rev 1013
29d29a54a208785e93d8ed2024e2222115ede489 v1.2.0 rev 1014
29d29a54a208785e93d8ed2024e2222115ede489 v1.2.0 rev 1014
0000000000000000000000000000000000000000 v1.2.0 rev 1014
abbf6ad98f25f0f33216ed11125a3a9cee8f90bc v1.2.1 rev 1014
6b017ddf90f21ab24a9b0f219657d48b1bdfc204 v1.2.1 rev 1015
fbc9d90492a631209d8587d299d08b0d005c681a v1.2.1 rev 1016
fd9ea977734c1bdbb37f40217becb4ec50d356d2 v1.2.1 rev 1017
b25021b3a495e039ef4a2a932a95df705648dc3c v1.2.0 rev 1010 prerelease
797ac8701d2c93b13c5cc585d39ff0a1a74d70d2 v1.2.0 rev 1011 prerelease
6e3dcf4780ac5e8a62b8d92f4111638cbc9a53e9 v1.2.0 rev 1009 prerelease
dd3c2816eb879b2b922d2e4c52bc95c8996a8f25 v1.2.0 rev 1008 RC2
06063863a95919cd18dc36841febbf21ca0fa60e v1.2.0 rev 1008 RC1
7f9a9e162ff5de00b4384de5a6bd7b0132ad1e9c v1.2.0 rev 1007 beta
a5a35a6eaf724a9c080a8ab85a2190f8fa6470fa v1.2.0 rev 1006 beta
eca30a5719a2d603c364d470bedfbd7c90c89e35 v1.2.0 rev 1005 beta
820c7e389d341953d0158290f880c9226d144870 v1.2.0 rev 1004 alpha
5630fb8134d1e8169517b5586b05cc4183370e56 v1.2.0 rev 1003 alpha
f628ef1bc45293e3d5fea77ac881a7b7652af554 v1.2.0 rev 1001 alpha
8c5544b1b0f4d469f35832c730d02a3bfbdb21ea v1.2.0 rev 1000 alpha
7f9a9e162ff5de00b4384de5a6bd7b0132ad1e9c v1.2.0 rev 1007 RC1
7f9a9e162ff5de00b4384de5a6bd7b0132ad1e9c v1.2.0 rev 1007 beta
0000000000000000000000000000000000000000 v1.2.0 rev 1007 beta
a5a35a6eaf724a9c080a8ab85a2190f8fa6470fa v1.2.0 rev 1006 RC1
a5a35a6eaf724a9c080a8ab85a2190f8fa6470fa v1.2.0 rev 1006 beta
0000000000000000000000000000000000000000 v1.2.0 rev 1006 beta
eca30a5719a2d603c364d470bedfbd7c90c89e35 v1.2.0 rev 1005 RC1
eca30a5719a2d603c364d470bedfbd7c90c89e35 v1.2.0 rev 1005 beta
0000000000000000000000000000000000000000 v1.2.0 rev 1005 beta
820c7e389d341953d0158290f880c9226d144870 v1.2.0 rev 1004 RC1
820c7e389d341953d0158290f880c9226d144870 v1.2.0 rev 1004 alpha
0000000000000000000000000000000000000000 v1.2.0 rev 1004 alpha
5630fb8134d1e8169517b5586b05cc4183370e56 v1.2.0 rev 1003 RC1
5630fb8134d1e8169517b5586b05cc4183370e56 v1.2.0 rev 1003 alpha
0000000000000000000000000000000000000000 v1.2.0 rev 1003 alpha
f628ef1bc45293e3d5fea77ac881a7b7652af554 v1.2.0 rev 1001 RC1
f628ef1bc45293e3d5fea77ac881a7b7652af554 v1.2.0 rev 1001 alpha
0000000000000000000000000000000000000000 v1.2.0 rev 1001 alpha
8c5544b1b0f4d469f35832c730d02a3bfbdb21ea v1.2.0 rev 1000 RC1
8c5544b1b0f4d469f35832c730d02a3bfbdb21ea v1.2.0 rev 1000 alpha
0000000000000000000000000000000000000000 v1.2.0 rev 1000 alpha
7be1f7e0a0dc36b67f8a584c7f420c240eefd5c6 v1.2.1 rev 1018 (plus swahili)
c11fe1aebaca2e213f71e61ad2337791d02723e0 v1.2.1 rev 1019
64e490522c2ef5ebc9af8a1a2c0dbf78bb213c06 v1.2.1 rev 1020
ea230f4ee12c303640c30090c30a9ac386324d5a v1.2.2 rev 1021
ea230f4ee12c303640c30090c30a9ac386324d5a v1.2.2 rev 1021
3b8a3847d1bea472176a27548d602b119d0e695d v1.2.2 rev 1021
3b8a3847d1bea472176a27548d602b119d0e695d v1.2.2 rev 1021
f714913194222fb579c5ef6e59092c788eeea300 v1.2.2 rev 1021
1759bdc402730ed7a16d26e7aba44aa43f822355 v1.2.2 rev 1023
cef8219f74248bd4d295e5c105b9f0ced3ac7f8d v1.3 rev 1025
a3d74ab96cdee3e972f7e507776a3834f497fbe0 v1.3 rev 1027
5c2ce96c94c6daf5277befaabf54ab74f088437b v1.3 rev 1029
1c8b890c69fbccc47de596c8592d21c7439646ba v1.3 rev 1030
e1e6c70eef9913497c65de897e229a6e0d871ad8 v1.4 rev 1033
68616e24e60d963334c762daba5784c7af763d76 v1.4 rev 1034
aa63d3a1f3295cf5a4a6bc74d8f1a4d4939fdd8b v1.4 rev 1035
db03f3497da8c76883e475a14a2b034e61e29c37 v1.4 rev 1036
db03f3497da8c76883e475a14a2b034e61e29c37 v1.4 rev 1036
8f77433258b376de5a6a7267df03ecc7cdecc1a4 v1.4 rev 1036
86afc4615411eaeee61271790b2bae5de7b94176 v1.4 rev 1037
3254b74a70d9c4f4088ba710fd6d7f542ffe166f v1.4 rev 1038
bc69898dc4ccb061079ee82f9b52da34de8400b7 v1.4.3 rev 1040
cd78ad21a3c8b1f106cdbb35a55ddfd1c2cf021f v1.4.3 rev 1041
7bf86c85d2dac5d6353bf2007a5c817654bbfdfc v1.4.3 rev 1042
753b33ca1fa800b517fcb6156d55370b986cedeb v1.4.4 rev 1044
753b33ca1fa800b517fcb6156d55370b986cedeb v1.4.4 rev 1044
c0d79cc86e2da93dfb03165a520de2c42b5cf061 v1.4.4 rev 1044
72d2e66877f36e0d3b607a6dbc4207c96dcc4c0c v1.4.4 rev 1045
712f8174b16bd95ece6bac433f871a19d59ea46c v1.4.4 rev 1046
5a92ad65ed775aa744b0abfe1aaa3ef756bd82f3 v1.4.5 rev 1048
5a92ad65ed775aa744b0abfe1aaa3ef756bd82f3 v1.4.5 rev 1048
1a6028ae2ce23b2eb50bd7ee3b2674effd2620af v1.4.5 rev 1048
aacc424db2a79c201abd6dcb481af830d017477f v1.4.6 rev 1049 for testing
aacc424db2a79c201abd6dcb481af830d017477f v1.4.6 rev 1049 for testing
1fced08f31e9e4f10fcdf067debdbad3557899de v1.4.6 rev 1049 for testing
================================================
FILE: LICENSE.md
================================================
Apache License
==============
_Version 2.0, January 2004_
_<>_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
================================================
FILE: README.md
================================================
# ODK Collect

[](https://opensource.org/licenses/Apache-2.0)
[](https://circleci.com/gh/getodk/collect)
[](https://slack.getodk.org)
ODK Collect is an Android app for filling out forms. It is designed to be used in resource-constrained environments with challenges such as unreliable connectivity or power infrastructure. ODK Collect is part the ODK project, a free and open-source set of tools which help organizations author, field, and manage mobile data collection solutions. Learn more about ODK and its history [here](https://getodk.org/) and read about example ODK deployments [here](https://forum.getodk.org/c/showcase).
ODK Collect renders forms that are compliant with the [ODK XForms standard](https://getodk.github.io/xforms-spec/), a subset of the [XForms 1.1 standard](https://www.w3.org/TR/xforms/) with some extensions. The form parsing is done by the [JavaRosa library](https://github.com/getodk/javarosa) which Collect includes as a dependency.
Please note that the `master` branch reflects ongoing development and is not production-ready.
## Table of Contents
* [Learn more about ODK Collect](#learn-more-about-odk-collect)
* [Release cycle](#release-cycle)
* [Downloading builds](#downloading-builds)
* [Suggesting new features](#suggesting-new-features)
* Contributing
* [Contributing code](#contributing-code)
* [Contributing translations](#contributing-translations)
* Developing
* [Setting up your development environment](#setting-up-your-development-environment)
* [Testing a form without a server](#testing-a-form-without-a-server)
* [Using APIs for local development](#using-apis-for-local-development)
* [Debugging JavaRosa](#debugging-javarosa)
* [Troubleshooting](#troubleshooting)
* [Test devices](#test-devices)
* [Creating signed releases for Google Play Store](#creating-signed-releases-for-google-play-store)
## Learn more about ODK Collect
* ODK website: [https://getodk.org](https://getodk.org)
* ODK Collect usage documentation: [https://docs.getodk.org/collect-intro/](https://docs.getodk.org/collect-intro/)
* ODK forum: [https://forum.getodk.org](https://forum.getodk.org)
* ODK developer Slack chat: [https://slack.getodk.org](https://slack.getodk.org)
## Release cycle
The work to be done is continuously revised and prioritized in [the backlog](https://github.com/orgs/getodk/projects/9/views/8) by the Collect team. The majority of this is influenced by the priorities in [the ODK roadmap](https://getodk.org/roadmap). Releases are planned to happen every 2-3 months (resulting in ~4 releases a year). This goal is to balance the pace of delivery with keeping things stable for users while also minimizing the risk in each release.
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).
Fixes to a previous release should be merged to a "release" branch (`v2023.2.x` for example) so as to leave `master` available for the current release's work. If hotfix changes are needed in the current release as well then these can be merged in as a PR after hotfix releases (generally easiest as a single PR for the whole hotfix release). This approach can also be used if work for the next release starts before the current one is out - the next release continues on `master` while the release is on a release branch.
At the beginning of each release cycle, [@grzesiek2010](https://github.com/grzesiek2010) updates all dependencies that have compatible upgrades available and ensures that the build targets the latest SDK.
## Downloading builds
Per-commit debug builds can be found on [CircleCI](https://circleci.com/gh/getodk/collect). Login with your GitHub account, click the build you'd like, then find the APK in the Artifacts tab.
If you are looking to use ODK Collect, we strongly recommend using the [Play Store build](https://play.google.com/store/apps/details?id=org.odk.collect.android). Current and previous production builds can be found in [Releases](https://github.com/getodk/collect/releases).
## Suggesting new features
We try to make sure that all issues in the issue tracker are as close to fully specified as possible so that they can be closed by a pull request. Feature suggestions should be described [in the forum Features category](https://forum.getodk.org/c/features) and discussed by the broader user community. Once there is a clear way forward, issues should be filed on the relevant repositories. More controversial features will be discussed as part of the Technical Steering Committee's [roadmapping process](https://github.com/getodk/governance/blob/master/TSC-1/STANDARD-OPERATING-PROCEDURES.md#roadmap).
## Contributing code
Any and all contributions to the project are welcome. ODK Collect is used across the world primarily by organizations with a social purpose so you can have real impact!
Issues tagged as [good first issue](https://github.com/getodk/collect/labels/good%20first%20issue) should be a good place to start. There are also currently many issues tagged as [needs reproduction](https://github.com/getodk/collect/labels/needs%20reproduction) which need someone to try to reproduce them with the current version of ODK Collect and comment on the issue with their findings.
If you're ready to contribute code, see [the contribution guide](docs/CONTRIBUTING.md).
## Contributing translations
If you know a language other than English, consider contributing translations through [Transifex](https://explore.transifex.com/getodk/collect/).
Translations are updated right before the first beta for a release and before the release itself. To update translations, download the zip from https://explore.transifex.com/getodk/collect/. The contents of each folder then need to be moved to the Android project folders. A quick script like [the one in this gist](https://gist.github.com/lognaturel/9974fab4e7579fac034511cd4944176b) can help. We currently copy everything from Transifex to minimize manual intervention. Sometimes translation files will only get comment changes. When new languages are updated in Transifex, they need to be added to the script above. Additionally, `ApplicationConstants.TRANSLATIONS_AVAILABLE` needs to be updated. This array provides the choices for the language preference in settings. Ideally the list could be dynamically generated.
## Setting up your development environment
1. Download and install [Git](https://git-scm.com/downloads) and add it to your PATH
1. Download and install [Android Studio](https://developer.android.com/studio/index.html)
1. Fork the collect project ([why and how to fork](https://help.github.com/articles/fork-a-repo/))
1. Clone your fork of the project locally. At the command line:
git clone https://github.com/YOUR-GITHUB-USERNAME/collect
If you prefer not to use the command line, you can use Android Studio to create a new project from version control using `https://github.com/YOUR-GITHUB-USERNAME/collect`.
1. Use Android Studio to import the project from its Gradle settings. To run the project, click on the green arrow at the top of the screen.
1. Windows developers: continue configuring Android Studio with the steps in this document: [Developing ODK Collect on Windows](docs/WINDOWS-DEV-SETUP.md).
1. Make sure you can run unit tests by running everything under `collect_app/src/test/java` in Android Studio or on the command line:
```
./gradlew testDebug
```
1. Make sure you can run instrumented tests by running everything under `collect_app/src/androidTest/java` in Android Studio or on the command line:
```
./gradlew connectedAndroidTest
```
**Note:** You can see the emulator setup used on CI in `.circleci/config.yml`.
## Customizing the development environment
### Changing JVM heap size
You can customize the heap size that is used for compiling and running tests. Increasing these will most likely speed up compilation and tests on your local machine. The default values are specified in the project's `gradle.properties` and this can be overriden by your user level `gradle.properties` (found in your `GRADLE_USER_HOME` directory). An example `gradle.properties` that would give you a heap size of 4GB (rather than the default 1GB) would look like:
```
org.gradle.jvmargs=-Xmx4096m
```
## Testing a form without a server
When you first run Collect, it is set to download forms from [https://demo.getodk.org/](https://demo.getodk.org/), the demo server. You can sometimes verify your changes with those forms but it can also be helpful to put a specific test form on your device. Here are some options for that:
1. The `All question types` form from the default server is [here](https://docs.google.com/spreadsheets/d/1af_Sl8A_L8_EULbhRLHVl8OclCfco09Hq2tqb9CslwQ/edit#gid=0). You can also try [example forms](https://github.com/XLSForm/example-forms) and [test forms](https://github.com/XLSForm/test-forms) or [make your own](https://xlsform.org).
1. Convert the XLSForm (xlsx) to XForm (xml). Use the [ODK website](http://getodk.org/xlsform/) or [XLSForm Offline](https://gumroad.com/l/xlsform-offline) or [pyxform](https://github.com/XLSForm/pyxform).
1. Once you have the XForm, use [adb](https://developer.android.com/studio/command-line/adb.html) to push the form to your device (after [enabling USB debugging](https://www.kingoapp.com/root-tutorials/how-to-enable-usb-debugging-mode-on-android.htm)) or emulator.
```
adb push my_form.xml /sdcard/Android/data/org.odk.collect.android/files/projects/{project-id}/forms
```
If you are using the demo project, kindly replace `{project_id}` with `DEMO`
4. Launch ODK Collect and tap `+ Start new form`. The new form will be there.
More information about using Android Debug Bridge with Collect can be found [here](https://docs.getodk.org/collect-adb/).
## Using APIs for local development
Certain functions in ODK Collect depend on cloud services that require API keys or authorization steps to work. Here are the steps you need to take in order to use these functions in your development builds.
**Google Maps API**: When the "Google Maps SDK" option is selected in the "User interface" settings, ODK Collect uses the Google Maps API for displaying maps in the geospatial question types (GeoPoint, GeoTrace, and GeoShape). To enable this API:
1. [Get a Google Maps API key](https://developers.google.com/maps/documentation/android-api/signup). Note that this requires a credit card number, though the card will not be charged immediately; some free API usage is permitted. You should carefully read the terms before providing a credit card number.
1. Edit or create `secrets.properties` and set the `GOOGLE_MAPS_API_KEY` property to your API key. You should end up with a line that looks like this:
```
GOOGLE_MAPS_API_KEY=AIbzvW8e0ub...
```
**Mapbox Maps SDK for Android**: When the "Mapbox SDK" option is selected in the "User interface" settings, ODK Collect uses the Mapbox SDK for displaying maps in the geospatial question types (GeoPoint, GeoTrace, and GeoShape). To enable this API:
1. [Create a Mapbox account](https://www.mapbox.com/signup/). Note that signing up with the "Pay-As-You-Go" plan does not require a credit card. Mapbox provides free API usage up to the monthly thresholds documented at [https://www.mapbox.com/pricing](https://www.mapbox.com/pricing). If your usage exceeds these thresholds, you will receive e-mail with instructions on how to add a credit card for payment; services will remain live until the end of the 30-day billing term, after which the account will be deactivated and will require a credit card to reactivate.
2. Find your access token on your [account page](https://account.mapbox.com/) - it should be in "Tokens" as "Default public token".
3. Edit or create `secrets.properties` and set the `MAPBOX_ACCESS_TOKEN` property to your access token. You should end up with a line that looks like this:
```
MAPBOX_ACCESS_TOKEN=pk.eyJk3bumVp4i...
```
4. Create a new secret token with the "DOWNLOADS:READ" secret scope and then add it to `secrets.properties` as `MAPBOX_DOWNLOADS_TOKEN`.
*Note: Mapbox will not be available as an option in compiled versions of Collect unless you follow the steps above. Mapbox will also not be available on x86 devices as the native libraries are excluded to reduce the APK size. If you need to use an x86 device, you can force the build to include x86 libs by include the `x86Libs` Gradle parameter. For example, to build a debug APK with x86 libs: `./gradlew assembleDebug -Px86Libs`.*
## Debugging JavaRosa
JavaRosa is the form engine that powers Collect. If you want to debug or change that code while running Collect you can deploy it locally with Maven (you'll need `mvn` and `sed` installed):
1. Build and install your changes of JavaRosa (into your local Maven repo):
```bash
./gradlew publishToMavenLocal
```
1. Change `const val javarosa = javarosa_online` in `Dependencies.kt` to `const val javarosa = javarosa_local`
## Troubleshooting
#### Error when running Robolectric tests from Android Studio on macOS: `build/intermediates/bundles/debug/AndroidManifest.xml (No such file or directory)`
> Configure the default JUnit test runner configuration in order to work around a bug where IntelliJ / Android Studio does not set the working directory to the module being tested. This can be accomplished by editing the run configurations, Defaults -> JUnit and changing the working directory value to $MODULE_DIR$.
> Source: [Robolectric Wiki](https://github.com/robolectric/robolectric/wiki/Running-tests-in-Android-Studio#notes-for-mac).
#### Android Studio Error: `SDK location not found. Define location with sdk.dir in the local.properties file or with an ANDROID_HOME environment variable.`
When cloning the project from Android Studio, click "No" when prompted to open the `build.gradle` file and then open project.
#### Execution failed for task ':collect_app:transformClassesWithInstantRunForDebug'.
We have seen this problem happen in both IntelliJ IDEA and Android Studio, and believe it to be due to a bug in the IDE, which we can't fix. As a workaround, turning off [Instant Run](https://developer.android.com/studio/run/#set-up-ir) will usually avoid this problem. The problem is fixed in Android Studio 3.5 with the new [Apply Changes](https://medium.com/androiddevelopers/android-studio-project-marble-apply-changes-e3048662e8cd) feature.
#### Moving to the main view if user minimizes the app
If you build the app on your own using Android Studio `(Build -> Build APK)` and then install it (from an `.apk` file), you might notice this strange behaviour thoroughly described: [#1280](https://github.com/getodk/collect/issues/1280) and [#1142](https://github.com/getodk/collect/issues/1142).
This problem occurs building other apps as well.
#### gradlew Failure: `FAILURE: Build failed with an exception.`
If you encounter an error similar to this when running `gradlew`:
```
FAILURE: Build failed with an exception
What went wrong:
A problem occurred configuring project ':collect_app'.
> Failed to notify project evaluation listener.
> Could not initialize class com.android.sdklib.repository.AndroidSdkHandler
```
You may have a mismatch between the embedded Android SDK Java and the JDK installed on your machine. You may wish to set your **JAVA_HOME** environment variable to that SDK. For example, on macOS:
`export JAVA_HOME="/Applications/Android\ Studio.app/Contents/jre/Contents/Home/"
`
Note that this change might cause problems with other Java-based applications (e.g., if you uninstall Android Studio).
#### gradlew Failure: `java.lang.NullPointerException (no error message).`
If you encounter the `java.lang.NullPointerException (no error message).` when running `gradlew`, please make sure your Java version for this project is Java 17.
This can be configured under **File > Project Structure** in Android Studio, or by editing `$USER_HOME/.gradle/gradle.properties` to set `org.gradle.java.home=(path to JDK home)` for command-line use.
#### `Unable to resolve artifact: Missing` while running tests
This is encountered when Robolectric has problems downloading the jars it needs for different Android SDK levels. If you keep running into this you can download the JARs locally and point Robolectric to them by doing:
```
./download-robolectric-deps.sh
```
## Test devices
Devices that @getodk/testers have available for testing are as follows:
* Xiaomi Redmi 9T 4GB - Android 10
* Pixel 7a 8GB - Android 14
* LG Nexus 5X 2GB - Android 8.1
* Samsung Galaxy M12 4GB - Android 11
* Samsung Galaxy M23 4GB - Android 14
* Xiaomi Redmi 7 3GB - Android 10
* Pixel 6a 6GB - Android 13
* Pixel 3a 4GB - Android 12
* Huawei Y560-L01 1GB - Android 5.1
## Creating signed releases for Google Play Store
Maintainers keep a folder with a clean checkout of the code and use [jenv.be](https://www.jenv.be) in that folder to ensure compilation with Java 17.
### Release prerequisites:
- a`local.properties` file in the root folder with the following:
```
sdk.dir=/path/to/android/sdk
```
- the keystore file and passwords
- a `secrets.properties` file in the root project folder folder with the following:
```
// secrets.properties
RELEASE_STORE_FILE=/path/to/collect.keystore
RELEASE_STORE_PASSWORD=secure-store-password
RELEASE_KEY_ALIAS=key-alias
RELEASE_KEY_PASSWORD=secure-alias-password
```
- a `google-services.json` file in the `collect_app/src/odkCollectRelease` folder. The contents of the file are similar to the contents of `collect_app/src/google-services.json`.
### Release checklist:
- update translations
- make sure CI is green for the chosen commit
- run `./gradlew releaseCheck`. If successful, a signed release will be at `collect_app/build/outputs/apk` (with an old version name)
- verify a basic "happy path": scan a QR code to configure a new project, get a blank form, fill it, open the form map (confirms that the Google Maps key is correct), send form
- run `./benchmark.sh` with a real device connected to verify performance
- To run benchmarks a project will need to be set up in Central with the benchmark forms and app users. The forms and entities needed for that are available [here](https://drive.google.com/drive/folders/1dPLvDY0LhVX-5qTUEs6EDoraDnLpUS0g?usp=drive_link).
- verify new APK can be installed as update to previous version and that above "happy path" works in that case also
- create and publish scheduled forum post with release description
- write Play Store release notes, include link to forum post
- when creating a major release:
- Tag the commit for the release (`vX.X.0`)
- Run `./create-release.sh `
- when creating a patch release:
- Tag the commit for the patch release (`vX.X.X`)
- (If beta has started for next release) Tag the commit for the beta release (`vX.X.X-beta.X`)
- Run `./create-release.sh `
- when creating a beta release:
- Tag the commit for the beta release (`vX.X.X-beta.X`)
- Run `./create-release.sh `
- add a release to Github [here](https://github.com/getodk/collect/releases), generate release notes and attach the APK
- upload APK(s) to Play Store
- When creating a hotfix, the beta APK should be uploaded second as it will have a higher version code
- backup dependencies for the release by downloading the `vX.X.X.tar` artifact from the `create_dependency_backup` job on Circle CI (for the release commit) and then uploading it to [this folder](https://drive.google.com/drive/folders/1_tMKBFLdhzFZF9GKNeob4FbARjdfbtJu?usp=share_link)
- backup a self signed release APK by downloading the `selfSignedRelease.apk` from the `build_release` job on Circle CI (for the release commit) and then upload to [this folder](https://drive.google.com/drive/folders/1pbbeNaMTziFhtZmedOs0If3BeYu3Ex5x?usp=share_link)
## Compiling a previous release using backed-up dependencies
1. Download the `.tar` for relevant release tag
2. Extract `.local-m2` into the project directory:
```bash
tar -xf maven.tar -C
```
The project will now be able to fetch dependencies that are no longer available (but were used to compile the release) from the `.local-m2` Maven repo.
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Reporting a Vulnerability
See our [Vulnerability Disclosure Policy](https://getodk.org/vdp).
================================================
FILE: analytics/.gitignore
================================================
/build
================================================
FILE: analytics/build.gradle.kts
================================================
plugins {
alias(libs.plugins.androidLibrary)
}
apply(from = "../config/quality.gradle")
android {
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
namespace = "org.odk.collect.analytics"
}
dependencies {
implementation(libs.kotlinStdlib)
implementation(libs.androidxCoreKtx)
implementation(libs.firebaseCrashlytics)
implementation(libs.firebaseAnalytics)
}
================================================
FILE: analytics/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: analytics/src/main/AndroidManifest.xml
================================================
================================================
FILE: analytics/src/main/java/org/odk/collect/analytics/Analytics.kt
================================================
package org.odk.collect.analytics
interface Analytics {
fun logEvent(event: String)
fun logEventWithParam(event: String, key: String, value: String)
fun logNonFatal(throwable: Throwable)
fun logMessage(message: String)
fun setAnalyticsCollectionEnabled(isAnalyticsEnabled: Boolean)
fun setUserProperty(name: String, value: String)
companion object {
private var instance: Analytics = NoopAnalytics()
private val params = mutableMapOf()
fun setInstance(analytics: Analytics) {
this.instance = analytics
}
@JvmStatic
fun log(event: String) {
instance.logEvent(event)
}
@JvmStatic
fun log(event: String, key: String) {
val paramValue = params[key]
if (paramValue != null) {
log(event, key, paramValue)
} else {
log(event)
}
}
@JvmStatic
fun log(event: String, key: String, value: String) {
instance.logEventWithParam(event, key, value)
}
@JvmStatic
fun setParam(key: String, value: String) {
params[key] = value
}
@JvmStatic
fun getParamValue(key: String): String? {
return params[key]
}
fun setUserProperty(name: String, value: String) {
instance.setUserProperty(name, value)
}
fun logNonFatal(throwable: Throwable) {
instance.logNonFatal(throwable)
}
}
}
================================================
FILE: analytics/src/main/java/org/odk/collect/analytics/BlockableFirebaseAnalytics.kt
================================================
package org.odk.collect.analytics
import android.app.Application
import android.os.Bundle
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
class BlockableFirebaseAnalytics(application: Application) : Analytics {
private val firebaseAnalytics = FirebaseAnalytics.getInstance(application)
private val crashlytics = FirebaseCrashlytics.getInstance()
override fun logEvent(event: String) {
firebaseAnalytics.logEvent(event, null)
}
override fun logEventWithParam(event: String, key: String, value: String) {
val bundle = Bundle()
bundle.putString(key, value)
firebaseAnalytics.logEvent(event, bundle)
}
override fun logNonFatal(throwable: Throwable) {
crashlytics.recordException(throwable)
}
override fun logMessage(message: String) {
crashlytics.log(message)
}
override fun setAnalyticsCollectionEnabled(isAnalyticsEnabled: Boolean) {
firebaseAnalytics.setAnalyticsCollectionEnabled(isAnalyticsEnabled)
crashlytics.setCrashlyticsCollectionEnabled(isAnalyticsEnabled)
}
override fun setUserProperty(name: String, value: String) {
firebaseAnalytics.setUserProperty(name, value)
}
}
================================================
FILE: analytics/src/main/java/org/odk/collect/analytics/NoopAnalytics.kt
================================================
package org.odk.collect.analytics
class NoopAnalytics : Analytics {
override fun logEvent(event: String) {}
override fun logEventWithParam(event: String, key: String, value: String) {}
override fun logNonFatal(throwable: Throwable) {}
override fun logMessage(message: String) {}
override fun setAnalyticsCollectionEnabled(isAnalyticsEnabled: Boolean) {}
override fun setUserProperty(name: String, value: String) {}
}
================================================
FILE: androidshared/.gitignore
================================================
/build
================================================
FILE: androidshared/build.gradle.kts
================================================
plugins {
alias(libs.plugins.androidLibrary)
alias(libs.plugins.kotlinKsp)
alias(libs.plugins.composeCompiler)
}
apply(from = "../config/quality.gradle")
android {
compileSdk = libs.versions.compileSdk.get().toInt()
buildFeatures {
viewBinding = true
compose = true
}
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
testOptions {
unitTests.isIncludeAndroidResources = true
}
namespace = "org.odk.collect.androidshared"
}
dependencies {
coreLibraryDesugaring(libs.desugar)
implementation(project(":icons"))
implementation(project(":strings"))
implementation(project(":shared"))
implementation(project(":async"))
implementation(libs.kotlinStdlib)
implementation(libs.androidxCoreKtx)
implementation(libs.androidxLifecycleLivedataKtx)
implementation(libs.androidMaterial)
implementation(libs.androidxFragmentKtx)
implementation(libs.androidxPreferenceKtx)
implementation(libs.timber)
implementation(libs.androidxExinterface)
implementation(libs.playServicesLocation)
val composeBom = platform(libs.androidxComposeBom)
implementation(composeBom)
implementation(libs.androidXComposeMaterial)
testImplementation(project(":test-shared"))
testImplementation(project(":androidtest"))
testImplementation(libs.junit)
testImplementation(libs.androidxTestExtJunit)
testImplementation(libs.androidxTestEspressoCore)
testImplementation(libs.robolectric)
testImplementation(libs.mockitoKotlin)
testImplementation(libs.androidxArchCoreTesting)
androidTestImplementation(libs.androidxTestExtJunit)
androidTestImplementation(libs.junit)
debugImplementation(project(":fragments-test"))
}
================================================
FILE: androidshared/src/androidTest/java/org/odk/collect/androidshared/bitmap/ImageCompressorTest.kt
================================================
/*
* 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.androidshared.bitmap
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.exifinterface.media.ExifInterface
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 java.io.File
@RunWith(AndroidJUnit4::class)
class ImageCompressorTest {
private lateinit var testImagePath: String
private val imageCompressor = ImageCompressor
@Test
fun imageShouldNotBeChangedIfMaxPixelsIsZero() {
saveTestBitmap(3000, 2000)
imageCompressor.execute(testImagePath, 0)
val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!
assertThat(3000, equalTo(image.width))
assertThat(2000, equalTo(image.height))
}
@Test
fun imageShouldNotBeChangedIfMaxPixelsIsSmallerThanZero() {
saveTestBitmap(3000, 2000)
imageCompressor.execute(testImagePath, -10)
val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!
assertThat(3000, equalTo(image.width))
assertThat(2000, equalTo(image.height))
}
@Test
fun imageShouldNotBeChangedIfMaxPixelsIsNotSmallerThanTheEdgeWhenWidthIsBiggerThanHeight() {
saveTestBitmap(3000, 2000)
imageCompressor.execute(testImagePath, 3000)
val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!
assertThat(3000, equalTo(image.width))
assertThat(2000, equalTo(image.height))
}
@Test
fun imageShouldNotBeChangedIfMaxPixelsIsNotSmallerThanTheLongEdgeWhenWidthIsSmallerThanHeight() {
saveTestBitmap(2000, 3000)
imageCompressor.execute(testImagePath, 4000)
val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!
assertThat(2000, equalTo(image.width))
assertThat(3000, equalTo(image.height))
}
@Test
fun imageShouldNotBeChangedIfMaxPixelsIsNotSmallerThanTheLongEdgeWhenWidthEqualsHeight() {
saveTestBitmap(3000, 3000)
imageCompressor.execute(testImagePath, 3000)
val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!
assertThat(3000, equalTo(image.width))
assertThat(3000, equalTo(image.height))
}
@Test
fun imageShouldBeCompressedIfMaxPixelsIsSmallerThanTheLongEdgeWhenWidthIsBiggerThanHeight() {
saveTestBitmap(4000, 3000)
imageCompressor.execute(testImagePath, 2000)
val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!
assertThat(2000, equalTo(image.width))
assertThat(1500, equalTo(image.height))
}
@Test
fun imageShouldBeCompressedIfMaxPixelsIsSmallerThanTheLongEdgeWhenWidthIsSmallerThanHeight() {
saveTestBitmap(3000, 4000)
imageCompressor.execute(testImagePath, 2000)
val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!
assertThat(1500, equalTo(image.width))
assertThat(2000, equalTo(image.height))
}
@Test
fun imageShouldBeCompressedIfMaxPixelsIsSmallerThanTheLongEdgeWhenWidthEqualsHeight() {
saveTestBitmap(3000, 3000)
imageCompressor.execute(testImagePath, 2000)
val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!
assertThat(2000, equalTo(image.width))
assertThat(2000, equalTo(image.height))
}
@Test
fun keepExifAfterScaling() {
val attributes = mutableMapOf(
// supported exif tags
ExifInterface.TAG_DATETIME to "2014:01:23 14:57:18",
ExifInterface.TAG_DATETIME_ORIGINAL to "2014:01:23 14:57:18",
ExifInterface.TAG_DATETIME_DIGITIZED to "2014:01:23 14:57:18",
ExifInterface.TAG_OFFSET_TIME to "+1:00",
ExifInterface.TAG_OFFSET_TIME_ORIGINAL to "+1:00",
ExifInterface.TAG_OFFSET_TIME_DIGITIZED to "+1:00",
ExifInterface.TAG_SUBSEC_TIME to "First photo",
ExifInterface.TAG_SUBSEC_TIME_ORIGINAL to "0",
ExifInterface.TAG_SUBSEC_TIME_DIGITIZED to "0",
ExifInterface.TAG_IMAGE_DESCRIPTION to "Photo from Poland",
ExifInterface.TAG_MAKE to "OLYMPUS IMAGING CORP",
ExifInterface.TAG_MODEL to "STYLUS1",
ExifInterface.TAG_SOFTWARE to "Version 1.0",
ExifInterface.TAG_ARTIST to "Grzegorz",
ExifInterface.TAG_COPYRIGHT to "G",
ExifInterface.TAG_MAKER_NOTE to "OLYMPUS",
ExifInterface.TAG_USER_COMMENT to "First photo",
ExifInterface.TAG_IMAGE_UNIQUE_ID to "123456789",
ExifInterface.TAG_CAMERA_OWNER_NAME to "John",
ExifInterface.TAG_BODY_SERIAL_NUMBER to "987654321",
ExifInterface.TAG_GPS_ALTITUDE to "41/1",
ExifInterface.TAG_GPS_ALTITUDE_REF to "0",
ExifInterface.TAG_GPS_DATESTAMP to "2014:01:23",
ExifInterface.TAG_GPS_TIMESTAMP to "14:57:18",
ExifInterface.TAG_GPS_LATITUDE to "50/1,49/1,8592/1000",
ExifInterface.TAG_GPS_LATITUDE_REF to "N",
ExifInterface.TAG_GPS_LONGITUDE to "0/1,8/1,12450/1000",
ExifInterface.TAG_GPS_LONGITUDE_REF to "W",
ExifInterface.TAG_GPS_SATELLITES to "8",
ExifInterface.TAG_GPS_STATUS to "A",
ExifInterface.TAG_ORIENTATION to "1",
// unsupported exif tags
ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH to "5",
ExifInterface.TAG_DNG_VERSION to "100"
)
saveTestBitmap(3000, 4000, attributes)
imageCompressor.execute(testImagePath, 2000)
val exifData = ExifInterface(testImagePath)
for (attributeName in attributes.keys) {
if (attributeName == ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH ||
attributeName == ExifInterface.TAG_DNG_VERSION
) {
assertThat(exifData.getAttribute(attributeName), equalTo(null))
} else {
assertThat(exifData.getAttribute(attributeName), equalTo(attributes[attributeName]))
}
}
}
@Test
fun verifyNoRotationAppliedForExifRotation() {
val attributes = mapOf(ExifInterface.TAG_ORIENTATION to ExifInterface.ORIENTATION_ROTATE_90.toString())
saveTestBitmap(3000, 4000, attributes)
imageCompressor.execute(testImagePath, 4000)
val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!
assertThat(3000, equalTo(image.width))
assertThat(4000, equalTo(image.height))
}
private fun saveTestBitmap(width: Int, height: Int, attributes: Map = emptyMap()) {
testImagePath = File.createTempFile("test", ".jpg").absolutePath
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
ImageFileUtils.saveBitmapToFile(bitmap, testImagePath)
val exifInterface = ExifInterface(testImagePath)
for ((key, value) in attributes) {
exifInterface.setAttribute(key, value)
}
exifInterface.saveAttributes()
}
}
================================================
FILE: androidshared/src/androidTest/java/org/odk/collect/androidshared/bitmap/ImageFileUtilsTest.kt
================================================
/*
* 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.androidshared.bitmap
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import androidx.exifinterface.media.ExifInterface
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import timber.log.Timber
import java.io.File
import java.io.IOException
@RunWith(AndroidJUnit4::class)
class ImageFileUtilsTest {
private lateinit var sourceFile: File
private lateinit var destinationFile: File
private lateinit var attributes: MutableMap
@Before
fun setup() {
sourceFile = createTempImageFile("source")
destinationFile = createTempImageFile("destination")
attributes = HashMap()
}
@Test
fun copyAndRotateImageNinety() {
attributes[ExifInterface.TAG_ORIENTATION] = ExifInterface.ORIENTATION_ROTATE_90.toString()
saveTestBitmapToFile(sourceFile.absolutePath, attributes)
ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile)
val image = ImageFileUtils.getBitmap(
destinationFile.absolutePath,
BitmapFactory.Options()
)!!
assertEquals(2, image.width)
assertEquals(1, image.height)
assertEquals(Color.GREEN, image.getPixel(0, 0))
assertEquals(Color.RED, image.getPixel(1, 0))
verifyNoExifOrientationInDestinationFile(destinationFile.absolutePath)
}
@Test
fun copyAndRotateImageTwoSeventy() {
attributes[ExifInterface.TAG_ORIENTATION] = ExifInterface.ORIENTATION_ROTATE_270.toString()
saveTestBitmapToFile(sourceFile.absolutePath, attributes)
ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile)
val image = ImageFileUtils.getBitmap(
destinationFile.absolutePath,
BitmapFactory.Options()
)!!
assertEquals(2, image.width)
assertEquals(1, image.height)
assertEquals(Color.RED, image.getPixel(0, 0))
assertEquals(Color.GREEN, image.getPixel(1, 0))
verifyNoExifOrientationInDestinationFile(destinationFile.absolutePath)
}
@Test
fun copyAndRotateImageOneEighty() {
attributes[ExifInterface.TAG_ORIENTATION] = ExifInterface.ORIENTATION_ROTATE_180.toString()
saveTestBitmapToFile(sourceFile.absolutePath, attributes)
ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile)
val image = ImageFileUtils.getBitmap(
destinationFile.absolutePath,
BitmapFactory.Options()
)!!
assertEquals(1, image.width)
assertEquals(2, image.height)
assertEquals(Color.GREEN, image.getPixel(0, 0))
assertEquals(Color.RED, image.getPixel(0, 1))
verifyNoExifOrientationInDestinationFile(destinationFile.absolutePath)
}
@Test
fun copyAndRotateImageUndefined() {
attributes[ExifInterface.TAG_ORIENTATION] = ExifInterface.ORIENTATION_UNDEFINED.toString()
saveTestBitmapToFile(sourceFile.absolutePath, attributes)
ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile)
val image = ImageFileUtils.getBitmap(
destinationFile.absolutePath,
BitmapFactory.Options()
)!!
assertEquals(1, image.width)
assertEquals(2, image.height)
assertEquals(Color.RED, image.getPixel(0, 0))
assertEquals(Color.GREEN, image.getPixel(0, 1))
verifyNoExifOrientationInDestinationFile(destinationFile.absolutePath)
}
@Test
fun copyAndRotateImageNoExif() {
saveTestBitmapToFile(sourceFile.absolutePath, null)
ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile)
val image = ImageFileUtils.getBitmap(
destinationFile.absolutePath,
BitmapFactory.Options()
)!!
assertEquals(1, image.width)
assertEquals(2, image.height)
assertEquals(Color.RED, image.getPixel(0, 0))
assertEquals(Color.GREEN, image.getPixel(0, 1))
verifyNoExifOrientationInDestinationFile(destinationFile.absolutePath)
}
/**
* These cases all have a window smaller than the image so the image should be scaled down.
* Note that the scaling isn't exact -- the factor is the closest power of 2 to the exact one.
*/
@Test
fun scaleDownBitmapWhenNeeded() {
runScaleTest(1000, 1000, 500, 500, 500, 500, false)
runScaleTest(600, 800, 600, 200, 150, 200, false)
runScaleTest(500, 400, 250, 200, 250, 200, false)
}
@Test
fun doNotScaleDownBitmapWhenNotNeeded() {
runScaleTest(1000, 1000, 2000, 2000, 1000, 1000, false)
runScaleTest(600, 800, 600, 800, 600, 800, false)
runScaleTest(500, 400, 600, 600, 500, 400, false)
runScaleTest(2000, 800, 4000, 2000, 2000, 800, false)
}
@Test
fun accuratelyScaleBitmapToDisplay() {
runScaleTest(1000, 1000, 500, 500, 500, 500, true)
runScaleTest(600, 800, 600, 200, 150, 200, true)
runScaleTest(500, 400, 250, 200, 250, 200, true)
runScaleTest(2000, 800, 300, 400, 300, 120, true)
runScaleTest(1000, 1000, 2000, 2000, 2000, 2000, true)
runScaleTest(600, 800, 600, 800, 600, 800, true)
runScaleTest(500, 400, 600, 600, 600, 480, true)
runScaleTest(2000, 800, 4000, 2000, 4000, 1600, true)
}
private fun runScaleTest(
imageHeight: Int,
imageWidth: Int,
windowHeight: Int,
windowWidth: Int,
expectedHeight: Int,
expectedWidth: Int,
shouldScaleAccurately: Boolean
) {
ScaleImageTest()
.createBitmap(imageHeight, imageWidth)
.scaleBitmapToDisplay(windowHeight, windowWidth, shouldScaleAccurately)
.assertScaledBitmapDimensions(expectedHeight, expectedWidth)
}
private fun verifyNoExifOrientationInDestinationFile(destinationFilePath: String) {
val exifData = getTestImageExif(destinationFilePath)
if (exifData != null) {
assertEquals(
ExifInterface.ORIENTATION_UNDEFINED,
exifData.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED
)
)
}
}
private fun saveTestBitmapToFile(
filePath: String,
attributes: Map?
) {
val bitmap = Bitmap.createBitmap(1, 2, Bitmap.Config.ARGB_8888)
bitmap.setPixel(0, 0, Color.RED)
bitmap.setPixel(0, 1, Color.GREEN)
ImageFileUtils.saveBitmapToFile(bitmap, filePath)
if (attributes != null) {
try {
val exifInterface = ExifInterface(filePath)
for (attributeName in attributes.keys) {
exifInterface.setAttribute(attributeName, attributes[attributeName])
}
exifInterface.saveAttributes()
} catch (e: IOException) {
Timber.w(e)
}
}
}
private fun getTestImageExif(imagePath: String): ExifInterface? {
try {
return ExifInterface(imagePath)
} catch (e: Exception) {
Timber.w(e)
}
return null
}
private fun createTempImageFile(imageName: String): File {
val temp = File.createTempFile(imageName, ".png")
temp.deleteOnExit()
return temp
}
private class ScaleImageTest {
private val cache = ApplicationProvider.getApplicationContext().externalCacheDir
private val imageFile = File(cache, "testImage.jpeg")
private var scaledBitmap: Bitmap? = null
fun createBitmap(imageHeight: Int, imageWidth: Int): ScaleImageTest {
val bitmap = Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.ARGB_8888)
ImageFileUtils.saveBitmapToFile(bitmap, imageFile.absolutePath)
return this
}
fun scaleBitmapToDisplay(
windowHeight: Int,
windowWidth: Int,
shouldScaleAccurately: Boolean
): ScaleImageTest {
scaledBitmap = ImageFileUtils.getBitmapScaledToDisplay(
imageFile,
windowHeight,
windowWidth,
shouldScaleAccurately
)
return this
}
fun assertScaledBitmapDimensions(expectedHeight: Int, expectedWidth: Int) {
assertEquals(expectedHeight.toLong(), scaledBitmap!!.height.toLong())
assertEquals(expectedWidth.toLong(), scaledBitmap!!.width.toLong())
}
}
}
================================================
FILE: androidshared/src/main/AndroidManifest.xml
================================================
================================================
FILE: androidshared/src/main/java/org/odk/collect/androidshared/async/TrackableWorker.kt
================================================
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
class TrackableWorker(private val scheduler: Scheduler) {
private val _isWorking = MutableNonNullLiveData(false)
val isWorking: NonNullLiveData = _isWorking
private var activeBackgroundJobsCounter = AtomicInteger(0)
fun immediate(background: Supplier, foreground: Consumer) {
activeBackgroundJobsCounter.incrementAndGet()
_isWorking.value = true
scheduler.immediate(background) { result ->
if (activeBackgroundJobsCounter.decrementAndGet() == 0) {
_isWorking.value = false
}
foreground.accept(result)
}
}
fun immediate(background: Runnable) {
immediate(
background = {
background.run()
},
foreground = {}
)
}
}
================================================
FILE: androidshared/src/main/java/org/odk/collect/androidshared/bitmap/ImageCompressor.kt
================================================
/*
* 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.androidshared.bitmap
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.exifinterface.media.ExifInterface
import timber.log.Timber
object ImageCompressor {
/**
* Before proceed with scaling or rotating, make sure existing exif information is stored/restored.
* @author Khuong Ninh (khuong.ninh@it-development.com)
*/
fun execute(imagePath: String, maxPixels: Int) {
backupExifData(imagePath)
scaleDownImage(imagePath, maxPixels)
restoreExifData(imagePath)
}
/**
* This method is used to reduce an original picture size.
* maxPixels refers to the max pixels of the long edge, the short edge is scaled proportionately.
*/
private fun scaleDownImage(imagePath: String, maxPixels: Int) {
if (maxPixels <= 0) {
return
}
var image = ImageFileUtils.getBitmap(imagePath, BitmapFactory.Options())
if (image != null) {
val originalWidth = image.width.toDouble()
val originalHeight = image.height.toDouble()
if (originalWidth > originalHeight && originalWidth > maxPixels) {
val newHeight = (originalHeight / (originalWidth / maxPixels)).toInt()
image = Bitmap.createScaledBitmap(image, maxPixels, newHeight, false)
ImageFileUtils.saveBitmapToFile(image, imagePath)
} else if (originalHeight > maxPixels) {
val newWidth = (originalWidth / (originalHeight / maxPixels)).toInt()
image = Bitmap.createScaledBitmap(image, newWidth, maxPixels, false)
ImageFileUtils.saveBitmapToFile(image, imagePath)
}
}
}
private fun backupExifData(imagePath: String) {
try {
val exif = ExifInterface(imagePath)
for ((key, _) in exifDataBackup) {
exifDataBackup[key] = exif.getAttribute(key)
}
} catch (e: Throwable) {
Timber.w(e)
}
}
private fun restoreExifData(imagePath: String) {
try {
val exif = ExifInterface(imagePath)
for ((key, value) in exifDataBackup) {
exif.setAttribute(key, value)
}
exif.saveAttributes()
} catch (e: Throwable) {
Timber.w(e)
}
}
private val exifDataBackup = mutableMapOf(
ExifInterface.TAG_DATETIME to null,
ExifInterface.TAG_DATETIME_ORIGINAL to null,
ExifInterface.TAG_DATETIME_DIGITIZED to null,
ExifInterface.TAG_OFFSET_TIME to null,
ExifInterface.TAG_OFFSET_TIME_ORIGINAL to null,
ExifInterface.TAG_OFFSET_TIME_DIGITIZED to null,
ExifInterface.TAG_SUBSEC_TIME to null,
ExifInterface.TAG_SUBSEC_TIME_ORIGINAL to null,
ExifInterface.TAG_SUBSEC_TIME_DIGITIZED to null,
ExifInterface.TAG_IMAGE_DESCRIPTION to null,
ExifInterface.TAG_MAKE to null,
ExifInterface.TAG_MODEL to null,
ExifInterface.TAG_SOFTWARE to null,
ExifInterface.TAG_ARTIST to null,
ExifInterface.TAG_COPYRIGHT to null,
ExifInterface.TAG_MAKER_NOTE to null,
ExifInterface.TAG_USER_COMMENT to null,
ExifInterface.TAG_IMAGE_UNIQUE_ID to null,
ExifInterface.TAG_CAMERA_OWNER_NAME to null,
ExifInterface.TAG_BODY_SERIAL_NUMBER to null,
ExifInterface.TAG_GPS_ALTITUDE to null,
ExifInterface.TAG_GPS_ALTITUDE_REF to null,
ExifInterface.TAG_GPS_DATESTAMP to null,
ExifInterface.TAG_GPS_TIMESTAMP to null,
ExifInterface.TAG_GPS_LATITUDE to null,
ExifInterface.TAG_GPS_LATITUDE_REF to null,
ExifInterface.TAG_GPS_LONGITUDE to null,
ExifInterface.TAG_GPS_LONGITUDE_REF to null,
ExifInterface.TAG_GPS_SATELLITES to null,
ExifInterface.TAG_GPS_STATUS to null,
ExifInterface.TAG_ORIENTATION to null
)
}
================================================
FILE: androidshared/src/main/java/org/odk/collect/androidshared/bitmap/ImageFileUtils.kt
================================================
package org.odk.collect.androidshared.bitmap
import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat
import android.graphics.BitmapFactory
import android.graphics.Matrix
import androidx.exifinterface.media.ExifInterface
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.lang.Exception
import java.util.Locale
import kotlin.math.ceil
object ImageFileUtils {
// 80% JPEG quality gives a greater file size reduction with almost no loss in quality
private const val IMAGE_COMPRESS_QUALITY = 80
private val EXIF_ORIENTATION_ROTATIONS = arrayOf(
ExifInterface.ORIENTATION_ROTATE_90,
ExifInterface.ORIENTATION_ROTATE_180,
ExifInterface.ORIENTATION_ROTATE_270
)
@JvmStatic
fun saveBitmapToFile(bitmap: Bitmap?, path: String) {
val compressFormat =
if (path.lowercase(Locale.getDefault()).endsWith(".png")) {
CompressFormat.PNG
} else {
CompressFormat.JPEG
}
try {
if (bitmap != null) {
FileOutputStream(path).use { out -> bitmap.compress(compressFormat, IMAGE_COMPRESS_QUALITY, out) }
}
} catch (e: Exception) {
Timber.e(e)
}
}
/*
This method is used to avoid OutOfMemoryError exception during loading an image.
If the exception occurs we catch it and try to load a smaller image.
*/
@JvmStatic
fun getBitmap(path: String?, originalOptions: BitmapFactory.Options): Bitmap? {
val newOptions = BitmapFactory.Options()
newOptions.inSampleSize = originalOptions.inSampleSize
if (newOptions.inSampleSize <= 0) {
newOptions.inSampleSize = 1
}
val bitmap: Bitmap? = try {
BitmapFactory.decodeFile(path, originalOptions)
} catch (e: OutOfMemoryError) {
Timber.i(e)
newOptions.inSampleSize++
return getBitmap(path, newOptions)
}
return bitmap
}
@JvmStatic
fun getBitmapScaledToDisplay(file: File, screenHeight: Int, screenWidth: Int): Bitmap? {
return getBitmapScaledToDisplay(file, screenHeight, screenWidth, false)
}
/**
* Scales image according to the given display
*
* @param file containing the image
* @param screenHeight height of the display
* @param screenWidth width of the display
* @param upscaleEnabled determines whether the image should be up-scaled or not
* if the window size is greater than the image size
* @return scaled bitmap
*/
@JvmStatic
fun getBitmapScaledToDisplay(
file: File,
screenHeight: Int,
screenWidth: Int,
upscaleEnabled: Boolean
): Bitmap? {
// Determine image size of file
var options = BitmapFactory.Options()
options.inJustDecodeBounds = true
getBitmap(file.absolutePath, options)
var bitmap: Bitmap?
val scale: Double
if (upscaleEnabled) {
// Load full size bitmap image
options = BitmapFactory.Options()
options.inInputShareable = true
options.inPurgeable = true
bitmap = getBitmap(file.absolutePath, options)
val heightScale = options.outHeight.toDouble() / screenHeight
val widthScale = options.outWidth.toDouble() / screenWidth
scale = widthScale.coerceAtLeast(heightScale)
val newHeight = ceil(options.outHeight / scale)
val newWidth = ceil(options.outWidth / scale)
if (bitmap != null) {
bitmap = Bitmap.createScaledBitmap(
bitmap,
newWidth.toInt(),
newHeight.toInt(),
false
)
}
} else {
val heightScale = options.outHeight / screenHeight
val widthScale = options.outWidth / screenWidth
// Powers of 2 work faster, sometimes, according to the doc.
// We're just doing closest size that still fills the screen.
scale = widthScale.coerceAtLeast(heightScale).toDouble()
// get bitmap with scale ( < 1 is the same as 1)
options = BitmapFactory.Options()
options.inInputShareable = true
options.inPurgeable = true
options.inSampleSize = scale.toInt()
bitmap = getBitmap(file.absolutePath, options)
}
if (bitmap != null) {
Timber.i(
"Screen is %dx%d. Image has been scaled down by %f to %dx%d",
screenHeight,
screenWidth,
scale,
bitmap.height,
bitmap.width
)
}
return bitmap
}
/**
* While copying the file, apply the exif rotation of sourceFile to destinationFile
* so that sourceFile with EXIF has same orientation as destinationFile without EXIF
*/
@JvmStatic
fun copyImageAndApplyExifRotation(sourceFile: File, destFile: File) {
var sourceFileExif: ExifInterface? = null
try {
sourceFileExif = ExifInterface(sourceFile)
} catch (e: IOException) {
Timber.w(e)
}
if (sourceFileExif == null ||
!EXIF_ORIENTATION_ROTATIONS.contains(
sourceFileExif
.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED
)
)
) {
// Source Image doesn't have any EXIF Rotations, so a normal file copy will suffice
sourceFile.copyTo(destFile, true)
} else {
val sourceImage = getBitmap(sourceFile.absolutePath, BitmapFactory.Options())
val orientation = sourceFileExif.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED
)
when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmapAndSaveToFile(
sourceImage,
90,
destFile.absolutePath
)
ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmapAndSaveToFile(
sourceImage,
180,
destFile.absolutePath
)
ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmapAndSaveToFile(
sourceImage,
270,
destFile.absolutePath
)
}
}
}
private fun rotateBitmapAndSaveToFile(image: Bitmap?, degrees: Int, filePath: String) {
var imageToSave = image
try {
val matrix = Matrix()
matrix.postRotate(degrees.toFloat())
if (image != null) {
imageToSave = Bitmap.createBitmap(image, 0, 0, image.width, image.height, matrix, true)
}
} catch (e: OutOfMemoryError) {
Timber.w(e)
}
saveBitmapToFile(imageToSave, filePath)
}
}
================================================
FILE: androidshared/src/main/java/org/odk/collect/androidshared/data/AppState.kt
================================================
package org.odk.collect.androidshared.data
import android.app.Activity
import android.app.Application
import android.app.Service
import android.content.Context
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* [AppState] can be used as a shared store of state that lives at an "app"/"in-memory" level
* rather than being tied to a specific component. This could be shared state between different
* [Activity] objects or a way of communicating between a [Service] and other components.
* [AppState] can be used as an alternative to Dagger singleton objects or static fields.
*
* [AppState] should not be used to share state between views or components on the same screen or make
* up part of the same flow. For this, using Jetpack's [ViewModel] at either a [Fragment] or [Activity]
* level is more appropriate.
*
* The easiest way to use [AppState] is have an instance owned by your app's [Application] object
* and implement the [StateStore] interface:
*
* ```
* class MyApplication : Application(), StateStore {
* private val appState = AppState()
* }
* ```
*
* The [AppState] instance can then be accessed anywhere the [Application] is available using the
* [getState] extension function.
*
*/
class AppState {
private val map = mutableMapOf()
@Suppress("UNCHECKED_CAST")
fun get(key: String, default: T): T {
return map.getOrPut(key) { default } as T
}
@Suppress("UNCHECKED_CAST")
fun get(key: String): T? {
return map[key] as T?
}
fun getFlow(key: String, default: T): StateFlow {
return get(key, MutableStateFlow(default))
}
fun set(key: String, value: Any?) {
map[key] = value
}
fun setFlow(key: String, value: T) {
get>(key).let {
if (it != null) {
it.value = value
} else {
map[key] = MutableStateFlow(value)
}
}
}
fun clear() {
map.clear()
}
fun clear(key: String) {
map.remove(key)
}
}
interface StateStore {
fun getState(): AppState
}
fun Application.getState(): AppState {
try {
val stateStore = this as StateStore
return stateStore.getState()
} catch (e: ClassCastException) {
throw ClassCastException("${this.javaClass} cannot be cast to StateStore")
}
}
fun Context.getState(): AppState {
return (applicationContext as Application).getState()
}
================================================
FILE: androidshared/src/main/java/org/odk/collect/androidshared/data/Consumable.kt
================================================
package org.odk.collect.androidshared.data
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
/**
* Useful for values that are read multiple times but only used
* once (like an error that shows a dialog once).
*/
data class Consumable(val value: T) {
private var consumed = false
fun isConsumed(): Boolean {
return consumed
}
fun consume() {
consumed = true
}
}
fun LiveData?>.consume(lifecycleOwner: LifecycleOwner, consumer: (T) -> Unit) {
observe(lifecycleOwner) { consumable ->
if (consumable != null && !consumable.isConsumed()) {
consumable.consume()
consumer(consumable.value)
}
}
}
================================================
FILE: androidshared/src/main/java/org/odk/collect/androidshared/data/Data.kt
================================================
package org.odk.collect.androidshared.data
import kotlinx.coroutines.flow.StateFlow
import org.odk.collect.androidshared.data.Updatable.Data
import org.odk.collect.androidshared.data.Updatable.QualifiedData
import kotlin.reflect.KProperty
sealed interface Updatable {
class QualifiedData(
private val appState: AppState,
private val key: String,
private val default: T
) : Updatable {
fun flow(qualifier: String): StateFlow {
return appState.getFlow("$qualifier:$key", default)
}
fun set(qualifier: String?, value: T) {
appState.setFlow("$qualifier:$key", value)
}
}
class Data(private val appState: AppState, private val key: String, private val default: T) :
Updatable {
fun flow(): StateFlow {
return appState.getFlow(key, default)
}
fun set(value: T) {
appState.setFlow(key, value)
}
}
}
abstract class DataService(
private val appState: AppState,
private val onUpdate: (() -> Unit)? = null
) {
private val updaters = mutableListOf>()
fun update(qualifier: String? = null) {
updaters.forEach { it.update(qualifier) }
onUpdate?.invoke()
}
protected fun data(key: String, default: T): DataDelegate {
val data = Data(appState, key, default)
return DataDelegate(data)
}
protected fun data(key: String, default: T, updater: () -> T): DataDelegate {
val data = Data(appState, key, default)
updaters.add(Updater(data) { updater() })
return DataDelegate(data)
}
protected fun qualifiedData(
key: String,
default: T
): QualifiedDataDelegate {
val data = QualifiedData(appState, key, default)
return QualifiedDataDelegate(data)
}
protected fun qualifiedData(
key: String,
default: T,
updater: (String) -> T
): QualifiedDataDelegate {
val data = QualifiedData(appState, key, default)
updaters.add(Updater(data) { it: String? -> updater(it!!) })
return QualifiedDataDelegate(data)
}
class QualifiedDataDelegate(private val data: QualifiedData) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): QualifiedData {
return data
}
}
class DataDelegate(private val data: Data) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): Data {
return data
}
}
private class Updater(
private val updatable: Updatable,
private val updater: (String?) -> T
) {
fun update(qualifier: String? = null) {
when (updatable) {
is Data -> updatable.set(updater(qualifier))
is QualifiedData -> updatable.set(qualifier, updater(qualifier))
}
}
}
}
================================================
FILE: androidshared/src/main/java/org/odk/collect/androidshared/livedata/LiveDataExt.kt
================================================
package org.odk.collect.androidshared.livedata
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
object LiveDataExt {
fun LiveData.combine(other: LiveData): LiveData> {
return LiveDataUtils.combine(this, other)
}
fun LiveData.runningFold(initial: U, operation: (U, T) -> U): LiveData {
val mediator = MediatorLiveData()
var accum = initial
mediator.addSource(this) {
accum = operation(accum, it)
mediator.value = accum
}
return mediator
}
/**
* Returns a [LiveData] where each value is a [Pair] made up of the latest value and the
* previous value.
*/
fun LiveData.withLast(): LiveData> {
return this.runningFold(Pair(null, null) as Pair) { last, current ->
Pair(last.second, current)
}
}
}
================================================
FILE: androidshared/src/main/java/org/odk/collect/androidshared/livedata/LiveDataUtils.java
================================================
package org.odk.collect.androidshared.livedata;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import org.odk.collect.async.Cancellable;
import java.util.function.Consumer;
import java.util.function.Function;
import kotlin.Pair;
import kotlin.Triple;
public class LiveDataUtils {
private LiveDataUtils() {
}
public static Cancellable observe(LiveData liveData, Consumer consumer) {
Observer observer = value -> {
if (value != null) {
consumer.accept(value);
}
};
liveData.observeForever(observer);
return () -> {
liveData.removeObserver(observer);
return true;
};
}
public static LiveData liveDataOf(T value) {
return new MutableLiveData<>(value);
}
public static LiveData> combine(LiveData one, LiveData two) {
return new CombinedLiveData<>(
new LiveData[]{one, two},
values -> new Pair<>((T) values[0], (U) values[1])
);
}
public static LiveData> combine3(LiveData one, LiveData two, LiveData three) {
return new CombinedLiveData<>(
new LiveData[]{one, two, three},
values -> new Triple<>((T) values[0], (U) values[1], (V) values[2])
);
}
public static LiveData> combine4(LiveData one, LiveData two, LiveData three, LiveData four) {
return new CombinedLiveData<>(
new LiveData[]{one, two, three, four},
values -> new Quad<>((T) values[0], (U) values[1], (V) values[2], (W) values[3])
);
}
private static class CombinedLiveData extends MediatorLiveData {
private final Object[] values;
private final Function